TypeScript泛型中类型收窄失效问题排查
自定义Schema类型提取问题:ExtractIdentity误匹配继承类属性
我在开发自定义Schema系统时,遇到了类型提取的问题:定义ExtractIdentity工具类型想要提取所有继承自SchemaIdentity<any>的属性名组成联合类型,但实际会把所有属性都提取出来,而非预期的仅id属性。
问题代码
export abstract class SchemaBase<T extends any> { } export class SchemaDefault<T extends any> extends SchemaBase<T> { } export class SchemaNumber<T extends number = number> extends SchemaBase<T> { } export class SchemaIdentity<T extends string | number> extends SchemaBase<T> { } export class SchemaDefinition<T extends {}> extends SchemaBase<T> { } export type ExtractIdentity<T> = T extends SchemaDefinition<infer R> ? { [K in keyof R]: R[K] extends SchemaIdentity<any> ? K : never; }[keyof R] : never; type MyModel = SchemaDefinition<{ id: SchemaIdentity<number>; name: SchemaDefault<string>; }> type MyIds = ExtractIdentity<MyModel> // 实际得到 "id" | "name",预期仅 "id"
问题原因
TypeScript使用结构类型系统,而非名义类型系统。因为SchemaDefault、SchemaIdentity等子类都继承自空的SchemaBase,没有任何额外的属性或方法,TypeScript会认为它们的结构完全相同,导致SchemaDefault<string> extends SchemaIdentity<any>返回true,从而错误地将name属性也纳入结果。
解决方案
给每个子类添加唯一的品牌属性,让TypeScript能通过结构区分不同的类:
export abstract class SchemaBase<T extends any> { readonly _schemaType: string; } export class SchemaDefault<T extends any> extends SchemaBase<T> { readonly _schemaType = "default"; } export class SchemaNumber<T extends number = number> extends SchemaBase<T> { readonly _schemaType = "number"; } export class SchemaIdentity<T extends string | number> extends SchemaBase<T> { readonly _schemaType = "identity"; } export class SchemaDefinition<T extends {}> extends SchemaBase<T> { readonly _schemaType = "definition"; } export type ExtractIdentity<T> = T extends SchemaDefinition<infer R> ? { [K in keyof R]: R[K] extends { _schemaType: "identity" } ? K : never; }[keyof R] : never; type MyModel = SchemaDefinition<{ id: SchemaIdentity<number>; name: SchemaDefault<string>; }> type MyIds = ExtractIdentity<MyModel> // 现在正确得到 "id"
或者用泛型参数作为品牌标识,同样能实现类型区分:
export abstract class SchemaBase<T extends any, Brand extends string> { readonly _brand: Brand; } export class SchemaDefault<T extends any> extends SchemaBase<T, "default"> {} export class SchemaNumber<T extends number = number> extends SchemaBase<T, "number"> {} export class SchemaIdentity<T extends string | number> extends SchemaBase<T, "identity"> {} export class SchemaDefinition<T extends {}> extends SchemaBase<T, "definition"> {} export type ExtractIdentity<T> = T extends SchemaDefinition<infer R> ? { [K in keyof R]: R[K] extends SchemaBase<any, "identity"> ? K : never; }[keyof R] : never;
添加品牌属性后,每个子类的结构不再相同,TypeScript就能准确判断R[K]是否属于SchemaIdentity类型,ExtractIdentity也就可以正确提取目标属性名。
内容的提问来源于stack exchange,提问作者Agrejus
相关产品推荐
相关产品推荐

