如何编写TypeScript泛型,确保子类型的id属性未被进一步约束?
我想要创建一个泛型函数,返回某类型的Partial<T>对象,功能是分配ID参数后返回该对象。针对具体类型时,我可以正常使用如下模式:
type Person = { id: string; name: string; } const generatePartialPerson = (): Partial<Person> => { const id = crypto.randomUUID() as string; const obj: Partial<Person> = { id }; return obj; }
但尝试编写可接受任何具有{id: string}属性的类型的泛型版本时,遇到了类型错误:
Type '{ id: string; }' is not assignable to type 'Partial
'
对应的泛型代码:
const generatePartialGeneric = <T extends { id: string }>( ): Partial<T> => { const id = crypto.randomUUID() as string; const obj: Partial<T> = { id }; return obj; };
我理解这是因为扩展{id: string}的类型可能会进一步约束id属性,比如设置为固定字符串字面量:
interface FixedPerson extends Person { id: "81a245c8-691e-471b-b07f-35e3ea3257ae"; }
请问是否可以编写一个泛型约束,确保类型/子类型必须具有未被进一步约束的{id: string}属性?
可以通过约束T['id']必须严格等于string类型来实现,而不是仅仅让T扩展{id: string}。具体来说,我们可以用条件类型验证T['id']是否就是string,而非它的子类型(比如字符串字面量)。
方法1:直接使用反向约束
修改泛型约束,要求string extends T['id'],确保T['id']不能是比string更具体的类型:
const generatePartialGeneric = <T extends { id: string } & (string extends T['id'] ? unknown : never)>( ): Partial<T> => { const id = crypto.randomUUID() as string; return { id } as Partial<T>; };
当传入的T的id是字符串字面量时,string extends T['id']会返回false,约束变为never,编译阶段直接报错,阻止这类类型传入。
方法2:封装约束为类型别名
如果觉得上面的写法繁琐,可以把约束封装成可复用的类型别名:
type HasUnconstrainedId<T> = T extends { id: string } ? string extends T['id'] ? T : never : never; const generatePartialGeneric = <T extends HasUnconstrainedId<T>>( ): Partial<T> => { const id = crypto.randomUUID() as string; return { id } as Partial<T>; };
验证效果
- 对于
Person这类id为string的正常类型,函数可正常调用:const partialPerson = generatePartialGeneric<Person>(); // 编译通过 - 对于
FixedPerson这类id是字面量的类型,直接触发编译错误:const partialFixedPerson = generatePartialGeneric<FixedPerson>(); // 错误:类型“FixedPerson”不满足约束“HasUnconstrainedId<FixedPerson>”
补充说明
这里需要用as Partial<T>做类型断言,是因为即使约束了T['id']为string,TypeScript类型系统仍无法直接推断{id: string}可以赋值给Partial<T>,但我们已经通过泛型约束确保了这个断言的安全性。
内容的提问来源于stack exchange,提问作者Ben Zittlau

