Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does TypeScript assertion of object literal `{a}` work with interface `{a, b}` but not `{a?, b}`

Why does the following assertion work:

interface AllRequired {
    a: string;
    b: string;
}

let all = {a: "foo"} as AllRequired; // No error

But this assertion gives an error:

interface SomeOptional {
    a?: string;
    b: string;
}

let some = {a: "foo"} as SomeOptional; // Error: Property 'b' missing

The only difference I can see is making one of the interface properties optional (?). It seems that if all properties are not optional, I can assert a partial object to the interface, but as soon as any of the interface properties are optional, I cannot assert a partial object anymore. This doesn't really make sense to me and I've been unable to find an explanation of this behavior. What's going on here?


For context: I encountered this behavior while trying to work around the problem that React's setState() takes a partial state object, but TypeScript doesn't yet have partial types to make this work properly with your state interface. As a workaround I came up with setState({a: "a"} as MyState) and found this works as long as interface MyState fields are all non-optional, but fails as soon as some properties are optional. (Making all properties optional is a workaround, but very undesirable in my case. )

like image 501
Aaron Beall Avatar asked Apr 15 '16 16:04

Aaron Beall


1 Answers

Type assertions can only be used to convert between a type and a subtype of it.

Let's say you declared the following variables:

declare var foo: number | string;
declare var bar: number;

Note number is a subtype of number | string, meaning any value that matches the type number (e.g. 3) also matches number | string. Therefore, it is allowed to use type assertions to convert between these types:

bar = foo as number; /* convert to subtype */
foo = bar as number | string; /* convert to supertype (assertion not necessary but allowed) */

Similarly, { a: string, b: string } is a subtype of { a: string }. Any value that matches { a: string, b: string } (e.g. { a: "A", b: "B" }) also matches { a: string }, because it has an a property of type string.

In contrast, neither of { a?: string, b: string } or { a: string } is a subtype of the other. Some values (e.g. { b: "B" }) match only the former, and others (e.g. { a: "A" }) match only the latter.

If you really need to convert between unrelated types, you can work around this by using a common supertype (such as any) as an intermediate:

let foo = ({ a: "A" } as any) as { a?: string, b: string };

The relevant part in the spec is 4.16 Type Assertions:

In a type assertion expression of the form < T > e, e is contextually typed (section 4.23) by T and the resulting type of e is required to be assignable to T, or T is required to be assignable to the widened form of the resulting type of e, or otherwise a compile-time error occurs.

like image 84
Spike Avatar answered Sep 27 '22 21:09

Spike