Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make TypeScript type check object literals with dynamic keys?

Tags:

typescript

I'm a bit confused on how TypeScript performs type checking when using dynamic keys in object literals. Consider the following two functions returning a copy of an object:

type Foo = {
  a: number;
  b: number;
};

const INIT_FOO: Foo = { a: 0, b: 0 };

function test1(k: keyof Foo) {
  const f: Foo = { ...INIT_FOO, [k]: true };
  return f
}

function test2(k: keyof Foo) {
  const f: Foo = { ...INIT_FOO };
  f[k] = true;
  return f
}

The TypeScript compiler will only report errors for function test2, and not function test1.

Why doesn't the compiler report an error for function test1 when it is clearly incorrect?

like image 568
ains Avatar asked Dec 07 '20 13:12

ains


People also ask

How do you dynamically access object property in TypeScript?

To dynamically access an object's property: Use keyof typeof obj as the type of the dynamic key, e.g. type ObjectKey = keyof typeof obj; . Use bracket notation to access the object's property, e.g. obj[myVar] .

How do you define a key of an object in TypeScript?

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.

Is Keyof TypeScript?

keyof is a keyword in TypeScript which is used to extract the key type from an object type.

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.

What is the difference between JavaScript and typescript?

In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types. As we’ve seen, they can be anonymous: or a type alias.

What is an anonymous type in typescript?

In TypeScript, we represent those through object types. As we’ve seen, they can be anonymous: or a type alias. In all three examples above, we’ve written functions that take objects that contain the property name (which must be a string) and age (which must be a number ).

What is dynamic type validation in typescript?

Dynamic type validation allows a type to generate a validator from its definition. Now they are related — a validator depends entirely on a type, preventing any mismatch between structures. To generate these validators, I found an amazing open-source project called typescript-json-validator, made by @ForbesLindesay.


1 Answers

This is a known issue, reported at microsoft/TypeScript#38663. It happens because computed properties where the key is a union of string literals (like keyof Foo) are widened to a string index signature. It's not exactly incorrect, but it is not specific enough to be useful. This behavior is either a bug, as reported in microsoft/TypeScript#13948, or possibly a design limitation, as described in microsoft/TypeScript#21030. In any case, this is the current behavior of the language.

Once the computed property is widened to an index signature, it becomes impossible to catch the bug. Index signatures are not considered excess properties; the type {a: number, b: number} & {[k: string]: boolean} (which is approximately what you get here) is assignable to {a: number, b: number}, so no bug is reported. But of course, in actuality, what you've done is assign a boolean to one of the a or b properties; it's just that the compiler can't see it because of the index signature widening.


Other than just making a note of this issue and being careful, the other thing you could do is manually implement a function that produces the type that "should" be generated when you have a computed property of a union of key types:

function computedProp<K extends PropertyKey, V>(
  key: K, val: V
): K extends any ? { [P in K]: V } : never {
  return { [key]: val } as any;
}

The return value is a distributive conditional type that produces a union of object types, like this:

const example = computedProp(Math.random() < 0.5 ? "a" : "b", true);
// const example: { a: boolean; } | { b: boolean; }

The union {a: boolean} | {b: boolean} is a more accurate representation of the type of {[k]: true}. If we use that, you get the expected error:

function test1(k: keyof Foo) {
  const f: Foo = { ...INIT_FOO, ...computedProp(k, true) }; // error!
  //    ~
  // '{ a: boolean; b: number; } | { b: boolean; a: number; }' is not assignable to 'Foo'.
  return f
}

Obviously using a function instead of a direct computed property is not really desirable, but at least you could sort of maintain type safety if it's important to you.

Playground link to code

like image 65
jcalz Avatar answered Oct 24 '22 14:10

jcalz