If you have a mapped type like this where keys in the form of {[K in keyof T]: ⋯}
are used, it signifies a homomorphic mapped type as discussed in this article on "homomorphic mapped types". Essentially, this means that optional and readonly properties from the input will carry over to the output type with optional properties always including undefined
.
In your case, the KeyPath<T>
type will include undefined
when any keys or subkeys (due to recursion) of T
are optional. You can verify this by testing the type against {} | null | undefined
, which is similar to unknown
:
type TestKey = KeyPath<Domain> & ({} | null | undefined);
// ^? type TestKey = "id" | "value" | "user" | "user.id" | "user.name" |
// "user.emails" | "user.isActive" | "user.type" | "user.name.first" |
// "user.name.last" | undefined
While TypeScript should ideally prevent using undefined
as a key in an indexed access type, there are cases where it doesn't catch such issues due to complex generics:
type Hmm<T> = { [K in keyof T]: K }[keyof T]
type Grr<T> = T[Hmm<T>]; // <-- I should be an error but it's not
type Okay = Grr<{a: string}> // string
type Bad = Grr<{ a?: string }> // unknown
The compiler ends up with unknown
when trying to resolve {a?: string}[undefined]
, despite the known limitation explained in this comment on microsoft/TypeScript#56515. To address this issue, a recommended fix is provided involving the use of the mapping modifier -?
:
To eliminate undefined
from the list of keys, the -?
modifier from the Required<T>
utility type is utilized as follows:
type KeyPath<T, TParentKey = undefined> = {
[K in keyof T]-?: K extends string ? (
// ^^
TParentKey extends undefined ? `${K}` : `${TParentKey & string}.${K}`
) : never; }[keyof T]
| {
[K in keyof T]-?: T[K] extends object ? (
// ^^
K extends string ? KeyPath<
T[K], TParentKey extends undefined ? `${K}` : `${TParentKey & string}.${K}`
> : never
) : never;
}[keyof T];
This modification ensures that undefined
is removed from keys, consequently fixing the issue with SortKey
:
type DomainSortKey = SortKey<Domain>;
/* type DomainSortKey = {
key: "id";
order: 'asc' | 'desc';
getter: Getter<Domain, "id">;
comparer: Comparer<number>;
} | {
key: "value";
order: 'asc' | 'desc';
getter: Getter<Domain, "value">;
comparer: Comparer<...>;
} | ... 7 more ... | {
...;
} */
Feel free to test out the code on the Playground link here