Take this example
interface Person<TName> {
name: TName;
}
type People =
| Person<"Levi">
| Person<"Julien">
type FilteredPersonBase<TPerson extends People> = TPerson["name"] extends "Levi" ? TPerson : never;
type FilteredPerson = FilteredPersonBase<People>; // never
type FilteredPersonBase2<TPerson extends People> = TPerson extends { name: "Levi" } ? TPerson : never;
type FilteredPerson2 = FilteredPersonBase2<People>; // Person<"Levi">
I would expect both FilteredPerson
and FilteredPerson2
to resolve to Person<"Levi">
, but they don't. Why does using the indexed access operator in the type constraint resolve differently than using the inline type?
I agree that the second one should resolve to person Person<"Levi">
, but lets look at how that happens:
FilteredPersonBase2<People>
// FilteredPersonBase2 is a distributive conditional type since
// TPerson appears nakedly in the condition of the conditional type
=> FilteredPersonBase2<Person<"Levi">> | FilteredPersonBase2<Person<"Julien">>
// The conditional type is applied to each member of the union
=> (Person<"Levi"> extends { name: "Levi" } ? Person<"Levi"> : never) | (Person<"Julien"> extends { name: "Levi" } ? Person<"Julien"> : never)
// The first application return Person<"Levi">, the second resolves to never
=> Person<"Levi"> | never
// Never is removed in a union
=> Person<"Levi">
The reason this works is the distributive behavior of conditional types
But conditional types only distribute over naked type parameters. In the first example the condition is over TPerson["name"]
which is not a naked type parameter, so no distribution occurs. Expanding the conditional type we get:
FilteredPersonBase<People> =>
// No distribution
People["name"] extends "Levi" ? People : never =>
// People["name"] just resolves to the union
"Levi" | "Julien" extends "Levi" ? People : never =>
// The union does not extend a constituent of the union
never
Since distribution does not occur, TPerson["name"]
just resolves to "Levi" | "Julien"
and a union does not extend a constituent. It would be true the other way around ("Levi" extends TPerson["name"]
) but that would just resolve to the union (Person
) since a constituent of a union is a sub-type of the union.
Just for fun, you could force distribution over TPerson
using an always true condition like TPerson extends TPerson
:
type FilteredPersonBase<TPerson extends People> = TPerson extends TPerson ? TPerson["name"] extends "Levi" ? TPerson : never : never;
type FilteredPerson = FilteredPersonBase<People>; // Person<"Levi">