Typescript throws an error when trying to pass a type with optional properties as an indexable type: (Playground)
type Thing = {
thing1?: string
thing2?: string
thing3?: number
}
const thing: Thing = {}
function processObject (obj: { [key: string]: string | number }): string {
/* Generic object handler, not specifically for Thing */
return "test"
}
console.assert(processObject(thing) === "test")
This results in:
Error: Argument of type 'Thing' is not assignable to parameter of type '{ [key: string]: string | number; }'. Property 'thing1' is incompatible with index signature. Type 'string | undefined' is not assignable to type 'string | number'. Type 'undefined' is not assignable to type 'string | number'.
Of course, it works if the argument's type is forcibly made optional:
function processObject (obj: { [key: string]: string | number | undefined }): string {
return "test"
}
I don't see why this is necessary, though. According to TS PR #7029, the indexable type should be compatible with other types that have the same implicit index signature.
Whether or not those properties are optional is irrelevant - that property should simply not be present on the object - right? Why do I have to specify undefined
? Is there a better way to be doing this?
I recommend using the index signature to annotate generic objects, e.g. keys are string type. But use Record<Keys, Type> to annotate specific objects when you know the keys in advance, e.g. a union of string literals 'prop1' | 'prop2' is used for keys.
Index signature can be used to define the type of the object whose values are of consistent types or you don't know the structure of the object you are dealing with.
TypeScript 4.4 brings support for static blocks in classes, an upcoming ECMAScript feature that can help you write more-complex initialization code for static members. These static blocks allow you to write a sequence of statements with their own scope that can access private fields within the containing class.
In TypeScript we can express its type as: ( a : number , b : number ) => number. This is TypeScript's syntax for a function's type, or call signature (also called a type signature). You'll notice it looks remarkably similar to an arrow function—this is intentional!
Consider that the following line will compile without warning according to your definition of Thing
:
const A: Thing = { thing2: undefined };
This makes the error when calling processObject()
technically correct; you might end up reading an undefined
value from the object when you expect only a string
or a number
.
Explanation:
TypeScript doesn't always distinguish situations where a value (like an object property or function parameter) is missing from those where the value is present but undefined
. This confusion is inherited from JavaScript, where the difference can be subtle: if I have a JavaScript object named obj
, and obj.prop === undefined
is true
, I can't tell if "prop" in obj
is true or false from that.
There's a longstanding open issue in GitHub, microsoft/TypeScript#13195, asking for some consistency around distinguishing "missing" from undefined
in TypeScript. Optional properties are treated like both "possibly missing" and "possibly undefined
", while the reverse, a required property with | undefined
in its definition is not allowed to be missing. Right now there's no way to say "I want a property that could be missing but if it is present it should not be undefined
".
Additionally, index signatures also suffer from doublethink in the opposite direction: in practice you can get undefined
property values from them (since any given property might be missing) but the compiler doesn't acknowledge this (and acts like every possible property is present and defined). See microsoft/TypeScript#13778 for the suggestion to include undefined
automatically in the domain of index signature properties, and for the zillions of comments about it. There is an upcoming feature flag called --noUncheckedIndexedAccess
(see microsoft/TypeScript#39560) which aims to address this, but even if you turn it on you still get the same problem with your code, so it doesn't fully fix it in all cases.
This mismatch between how missing and undefined
values are dealt with in optional properties versus how they are dealt with in index signatures is the cause of your problem.
In my opinion the way to deal with this is just to add that undefined
to your index signature and explicitly acknowledge that optional properties might be present but undefined
. It's not ideal, but is at least closer to consistent that way.
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With