Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is TypeScript Not Checking the Type of Dynamic Key Object Fields

Why does TypeScript accept the definition of seta when it does not return objects of type A?

type A = {
    a: '123',
    b: '456'
}

// Returns copy of obj with obj[k] = '933'
function seta<K extends keyof A>(k: K, obj: A): A {
    return {
        ...obj,
        [k]: '933'
    }
}

const w: A = seta('a', {
    a: '123',
    b: '456'
})

// now w = {a: '933', b: '456'}

https://tsplay.dev/wEGX4m

like image 909
Eugene Wolffe Avatar asked Dec 14 '21 22:12

Eugene Wolffe


People also ask

How do you define type of key in an object?

Use the keyof typeof syntax to create a type from an object's keys, e.g. type Keys = keyof typeof person . The keyof typeof syntax returns a type that represents all of the object's keys as strings. index.ts.

How do you define a type with dynamic keys in typescript?

Define a Type for Object with Dynamic keys in TypeScript# Use an index signature to define a type for an object with dynamic keys, e.g. [key: string]: string;. Index signatures are used when we don't know all of the names of a type's properties ahead of time, but know the shape of the values.

Why can't I use myVar dynamically in typescript?

The type of myVar is a string, and not all strings are properties of the object, so TypeScript informs us that we can't safely access the property dynamically. If you try to set the type of myVar to be a union of the object's keys, you would still get an error. The easiest way to get around this is to use a type assertion.

How to check the type of an object in typescript?

Checking the type of an object in Typescript: the type guards. Coming from a JS background, checking the type of an object in Typescript is kind of obscure at first. We are used to if (obj.property) {//obj.property exists here !} and this is not possible in Typescript…. In some cases, you can use type guards.

How do I access the object's property dynamically in typescript?

Use bracket notation to access the object's property, e.g. obj [myVar]. The keyof typeof syntax allows us to get a union type of the object's keys. This way we can inform TypeScript that the myVar variable will only ever store a string that is equal to one of the keys in the object. Now we can access the object property dynamically.


Video Answer


2 Answers

This looks like a bug or limitation in TypeScript; see microsoft/TypeScript#37103 (labeled a bug) or microsoft/TypeScript#32236 (labeled "needs investigation").

When you spread properties into an object literal and then add a computed property, it seems that TypeScript will completely ignore the computed property unless the computed key is of a single, specific, literal type (not a union of literals, and not a generic type parameter constrained to a string literal):

function testing<K extends "a" | "b">(a: "a", x: string, y: "a" | "b", z: K) {
    const someObject = { a: "v" } // { a: string }
    const objA = { ...someObject, [a]: 0 } // { a: number } πŸ‘
    const objX = { ...someObject, [x]: 0 } // { a: string } πŸ‘Ž
    const objY = { ...someObject, [y]: 0 } // { a: string } πŸ‘Ž
    const objZ = { ...someObject, [z]: 0 } // { a: string } πŸ‘Ž
}

Computed property keys in general are a bit problematic in TypeScript, even without spreading, as they tend to get widened all the way to string, losing information you might care about (see another bug at microsoft/TypeScript#13948):

function testing2<K extends "a" | "b">(a: "a", x: string, y: "a" | "b", z: K) {
    const objA = { [a]: 0 } // { a: number } πŸ‘
    const objX = { [x]: 0 } // { [x: string]: number; } πŸ€·β€β™‚οΈ
    const objY = { [y]: 0 } // { [x: string]: number; } πŸ€·β€β™‚οΈ
    const objZ = { [z]: 0 } // { [x: string]: number; } πŸ€·β€β™‚οΈ
}

So that's the problem you're having here.


I don't know what to say other than "tread lightly around computed properties". You can kind of work around it by defining your own function that produces a more "correct" version of what a computed property would be:

function kv<K extends PropertyKey, V>(k: K, v: V) {
    return { [k]: v } as { [P in K]: { [Q in P]: V } }[K]
}

const k = Math.random() < 0.5 ? "a" : "b";

const obj = kv(k, Math.random());
/* const obj: {
    a: number;
} | {
    b: number;
} */

You can see that a union of keys produces a union of objects. But if the key is generic then the compiler leaves that unevaluated and spread with generics tends to be represented as an intersection, which is often acceptable but not when keys overlap. (See microsoft/TypeScript#32022 for that issue):

function setA2<K extends keyof A>(k: K, obj: A): A {
    return {
        ...obj,
        ...kv(k, '933')
    } 
    /* const ret1: A & { [P in K]: { [Q in P]: string; }; }[K] */ // no error
}

So you'd have to widen k from K to keyof A before you got any warning:

function setA3<K extends keyof A>(k: K, obj: A): A {
    const kW: keyof A = k;
    return {
        ...obj,
        ...kv(kW, '933')
    }; // FINALLY AN ERROR
    // Type '{ a: string; b: "456"; } | { b: string; a: "123"; }' is not assignable to type 'A'.
}

So, hooray, I guess... you can spend a lot of effort to wrestle some type safety out of this function, but it's quite a pyrrhic victory (I think that's a word). So again, be careful around computed properties until and unless the relevant GitHub issues are fixed.

Playground link to code

like image 117
jcalz Avatar answered Nov 15 '22 08:11

jcalz


It's accepted because if you hover on [k]: '933', it says

(parameter) k: K extends keyof A

Which means you're returning a property that's extended from the return type, which is allowed.

When you declare that an object must be of an interface, you are saying that it must have at least those properties, not only those properties.

like image 40
Wolfgang Avatar answered Nov 15 '22 07:11

Wolfgang