TypeScript's generic constraints don't currently support any kind of lower bounds, as requested in microsoft/TypeScript#14520. So you can say T extends X
to mean that T
has to be a subtype of X
(so X
is an upper bound of T
), but you can't write T super X
to mean that T
has to be a supertype of X
(so X
is a lower bound of T
). Lower bounds are useful when trying to safely represent property writes instead of reads.
If T super X
existed in TypeScript, then you could maybe write your function like
declare const generatePartialGeneric:
<T extends {id: S}, S super string>() => Partial<T>;
but for now it's not part of the language.
Luckily you can simulate such a constraint with conditional types, as shown here:
const generatePartialGeneric =
<T extends { id: string extends T["id"] ? unknown : never }>(
): Partial<T> => {
const id: string = crypto.randomUUID();
const obj = { id } as Partial<T>;
return obj;
};
This will end up comparing T
against itself and will work if and only if it has an id
property which is a supertype of string
. Like this:
type Person = {
id: string;
name: string;
}
const x = generatePartialGeneric<Person>(); // okay
interface Oops {
id: "oops"
abc: string;
}
const y = generatePartialGeneric<Oops>(); // error
interface Okay {
id: string | number
abc: string;
}
const z = generatePartialGeneric<Okay>(); // okay
Note that in the implementation I had to use a type assertion at const obj = { id } as Partial<T>
instead of annotating it like const obj: Partial<T> = { id }
.
That's because the simulated lower bound is well beyond the compiler's ability to analyze inside the generic function implementation. The compiler really has no idea what might or might not be assignable to Partial<T>
, so it complains if we try. And that complaint is the same error as from your original code. The reason the error exists is because of the possibility of string
being too wide for the id
property, as you mentioned and as described in Why can't I return a generic 'T' to satisfy a Partial<T>? ... but even if you specifically craft the function so that this possibility doesn't exist, the compiler just can't see it. It's too abstract.
So we use a type assertion and move on.
There are other approaches to the sort of thing you're doing; one is to not try to capture the id
issue at all in T
, and instead have generatePartialGeneric
return a value whose type is related to T
. We know that the return value will be of type {id: string}
as well as something like Partial<Omit<T, "id">>
. That is, no matter what T
's id
property is, the output will be of type string
. That could be written like:
const generatePartialGeneric = <T extends object>() => {
const id = crypto.randomUUID() as string;
const obj: { [K in keyof T as Exclude<K, "id">]?: T[K] } = {};
return Object.assign(obj, { id });
};
type Person = {
id: string;
name: string;
}
const p = generatePartialGeneric<Person>();
/* const p: { name?: string | undefined; } & { id: string; } */
const q: Partial<Person> = generatePartialGeneric<Person>();
There are no issues with constraints at all here, because we are not worried about what T
might or might not be doing with id
. You could call generatePartialGeneric<{a: number}>
and you'd get a {a?: number; id: string}
out.
Playground link to code