#Map.set requires explicit value, even though undefined is accepted

26 messages · Page 1 of 1 (latest)

nocturne egretBOT
#
m.a.t.t.t#0

Preview:```ts
var map = new Map<number, string | undefined>()

map.set(1, "a")
map.set(2, undefined)
map.set(3)```

runic folio
#

Not everything that is possible to do in JS, is a good idea to do.

granite mauve
#

Map.set NEEDS a key and a value. The fact that you're allowing undefined doesn't change that.

barren crescent
#

It does work in the JS runtime

#

And also in JS I don’t believe there is a way for the callee to know whether the second parameter was explicitly passed as undefined, or omitted at the call site

#

So it’s not technically needed for that reason

granite mauve
#

Yeah, TypeScript does need it tho, since Map is defined as <K,V>Map

#

You could write a extension on Map

#

Or a custom map

runic folio
#

You can also declaration merge into it.

#

But again, just because you can do it in JS, doesn't mean it's a good idea.

barren crescent
#

Typescript shouldn’t be imposing opinions like that though, it should just be a matter of record for how JS actually works

#

Save the opinions for linters etc

vague raven
granite mauve
#

Especially since the example given explicitly says value needs to be a string or undefined

vague raven
granite mauve
#
class Mep {
  _mep: Record<PropertyKey, string | undefined> = {}
  set(key: PropertyKey, value?: string | undefined) {
    this._mep[key] = value || undefined
  }
}

const map = new Mep()

map.set(1, 'a')
map.set(2, undefined)
map.set(3)
#

Now you can.. yay

runic folio
#

You don't even need to do that, just declaration merge into it.

#
interface Map<K, V> {
    set(key: undefined extends V ? K : never): Map<K, V>
}

const map1 = new Map<string, number>()
map1.set('foo', 42)
map1.set('bar') // should error

const map2 = new Map<string, number | undefined>()
map2.set('foo', 42)
map2.set('bar') // should pass
#

!ts

nocturne egretBOT
#
interface Map<K, V> {
    set(key: undefined extends V ? K : never): Map<K, V>
}

const map1 = new Map<string, number>()
map1.set('foo', 42)
map1.set('bar') // should error
//       ^^^^^
// Argument of type 'string' is not assignable to parameter of type 'never'.

const map2 = new Map<string, number | undefined>()
map2.set('foo', 42)
map2.set('bar') // should pass
runic folio
#

But yes, don't do this, it's bad.

granite mauve
#

Ah, neat! But bad yeah

barren crescent
# vague raven It's not "typescript's opinion", the types have been written while accounting fo...

in the ecmascript spec, arguments are neither required nor optional. the spec will always be explicit about what should occur if a parameter is undefined. e.g. radix in parseInt, or when passing nothing to Object.keys() - its just the nature of the language

of course typescript should not include declarations where the result will cause an error e.g. Object.keys(undefined) or Object.keys()

but in the case of map.set(1) vs map.set(1, undefined) -- not only does the command not crash, it produces the desired behaviour because the implementation is spec-compliant

vague raven
# barren crescent in the ecmascript spec, arguments are neither required nor optional. the spec wi...

It is very explicit about arguments being optional: they are in brackets, eg:
https://tc39.es/ecma262/multipage/keyed-collections.html#sec-map.prototype.foreach
thisArg is explicitly optional.

https://tc39.es/ecma262/multipage/ecmascript-standard-built-in-objects.html#sec-ecmascript-standard-built-in-objects

Unless otherwise specified, [a function's length] is the number of required parameters shown in the subclause heading for the function description. Optional parameters and rest parameters are not included in the parameter count.