You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:47:24