Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Coercing type with optional properties to an indexable type

Tags:

typescript

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?

like image 261
snazzybouche Avatar asked Oct 23 '20 18:10

snazzybouche


People also ask

When to use index Signatures TypeScript?

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.

When to use index signature?

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.

What's new TypeScript 4. 4?

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.

What is a signature in TypeScript?

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!


1 Answers

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

like image 142
jcalz Avatar answered Oct 25 '22 21:10

jcalz