Typescript generic syntax for merge

type OptionalPropertyNames<T> =
  { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) } [keyof T];

I understand most of it but can't make sense of the :

{} extends { [P in K]: T[K] } ? K : never

what does that code block represent? Under which condition would the conditional be truthy?

Found the syntax in a StackOverflow post


The intent of OptionalPropertyNames<T> is to find the keys of T which correspond to optional properties, and we can see that it works (at least for object types without index signatures or other edge cases):

type OptionalPropertyNames<T> = { [K in keyof T]-?:
    ({} extends { [P in K]: T[K] } ? K : never)
}[keyof T];

interface Foo {
    opt?: string,
    req: string | undefined
}

type Opt = OptionalPropertyNames<Foo> // "opt"

This works by looking at each property K from the keys in T, and checking whether, essentially {} extends Pick<T, K> (the type {[P in K]: T[K]} is equivalent to the Pick<T, K> utility type at least in this instance where K extends keyof T). So for the opt property we're checking {} extends {opt?: string} and for the req property we're checking {} extends {req: string | undefined}.

For {} extends {req: string | undefined} this is obviously false. If you have a value of type {} you cannot safely treat it as if it's a value of type {req: string | undefined}. For {} extends {opt?: string}, though, TypeScript considers that to be true. That lets us distinguish between required and optional properties, and therefore OptionalPropertyNames<Foo> is "opt".

That's the answer to the question as asked. But wait, why is {} extends {opt?: string} true?


Well, it's very convenient to allow assigning an object type without a known property to an object type with an optional property:

interface Bar {
    req: string | undefined
}
let bar: Bar = { req: "req" };

interface Foo extends Bar {
    opt?: string;
}
let foo: Foo = bar; // allowed by convenience

It would be quite annoying if that last line failed. We know what bar is a valid Foo because bar is missing the opt property. The compiler sort of shrugs and says "well, the type Bar doesn't have a known opt property, so that's kind of like saying it is missing an opt property, which is compatible with the optional property from Foo". But that's not really a sound assumption to make.

Object types in TypeScript are open and extendible as opposed to sealed or exact (as requested in microsoft/TypeScript#12936), in order to support structural subtyping. An object can always turn out to have more properties than are to be found in the object's type:

interface Baz extends Bar {
    opt: number;
}
let baz: Baz = { opt: 123, req: "req" };
bar = baz; // allowed by structural subtyping

Here a value with a numeric opt property has been assigned to bar, and that assignment is legal and type safe by structural subtyping. But put together, these two rules can lead to trouble. While the compiler rightfully complains about this assignment:

foo = baz; // error! Types of property 'opt' are incompatible.

It fails to notice if you do the same assignment in two steps:

bar = baz; // allowed by subtyping
foo = bar; // allowed by convenience

And that means everything compilers just fine and you don't know anything's wrong until runtime:

if (foo.opt) {
    foo.opt.toUpperCase(); // no compiler error, but:
    // 💥 RUNTIME ERROR: foo.opt.toUpperCase is not a function
}

Oops.

So this is one of the places where TypeScript's type system is inconsistent. (See microsoft/TypeScript#42479.) and the above definition uses it to detect optional properties.


In the annoying alternate universe where the compiler did not allow such an assignment, you could probably still detect optional properties with something like:

type OptionalPropertyNames<T> = { [K in keyof T]-?:
    (Partial<Pick<T, K>> extends Pick<T, K> ? K : never)
}[keyof T];

since the Partial<T> utility type should produce something not assignable to T unless T already has all optional properties. But this is even more of a digression than the last section, so I'll stop here.

Playground link