基于类成员关联的TypeScript类型推断与约束实现
TypeScript 关联类型类的优化实现
需求概述
- 类
Foo的fieldOne类型为number | string,fieldTwo类型与fieldOne强关联:- 当
fieldOne是number时,fieldTwo必须是Bar[] - 当
fieldOne是string时,fieldTwo必须是Baz[]
- 当
- 构造函数传入两个字段,编译器需约束只能使用合法的类型组合实例化
- 类内大部分逻辑基于
Bar和Baz的联合类型,需支持在特定逻辑中收窄fieldTwo的类型到Bar[]或Baz[]
基础接口定义
interface Bar { foo: string; bar: number; } interface Baz { foo: string; baz: number; }
已尝试方案及问题
1. 继承方案
通过抽象基类+子类实现类型约束,但需要创建多个子类,结构繁琐:
abstract class Foo<T extends number | string, U extends Bar[] | Baz[]> { private fieldOne: T; protected fieldTwo: U; constructor(paramOne: T, paramTwo: U) { this.fieldOne = paramOne; this.fieldTwo = paramTwo; } generalFunction() { console.log(this.fieldTwo.map(x => x.foo).join()); this.specificFunction(); } protected abstract specificFunction(): void; } class FooBar extends Foo<number, Bar[]> { specificFunction() { console.log(this.fieldTwo.map(x => x.bar).join()); } } class FooBaz extends Foo<string, Baz[]> { specificFunction() { console.log(this.fieldTwo.map(x => x.baz).join()); } }
2. 条件类型方案
尝试用泛型条件类型关联字段,但未实现类型收窄:
type FooContents<T extends number | string> = T extends number ? Bar : Baz; class Foo<T extends number | string> { private fieldOne: T; private fieldTwo: FooContents<T>; constructor(param1: T, param2: FooContents<T>) { this.fieldOne = param1; this.fieldTwo = param2; } generalFunction() { console.log(this.fieldTwo.foo); } specificFunction() { // 无法自动收窄fieldTwo类型 } }
3. 联合参数对象方案
将字段合并为联合类型对象,但需要额外类型守卫函数,代码冗余(已修正原代码中的判断错误):
type FooParams = {one: number, two: Bar[]} | {one: string, two: Baz[]}; class Foo { params: FooParams; constructor(params: FooParams) { this.params = params; } generalFunction() { console.log(this.params.two.map(x => x.foo).join()); } specificFunction() { if (isNumberyBary(this.params)) this.processBars(this.params.two); if (isStringyBazy(this.params)) this.processBazs(this.params.two); } processBars(bars: Bar[]) { console.log(bars.map(x => x.bar).join()); } processBazs(bazs: Baz[]) { console.log(bazs.map(x => x.baz).join()); } } function isNumberyBary(x: FooParams): x is {one: number, two: Bar[]} { return typeof x.one === 'number'; } function isStringyBazy(x: FooParams): x is {one: string, two: Baz[]} { return typeof x.one === 'string'; }
优化实现方案
利用泛型条件类型+类内类型守卫,实现简洁且类型安全的方案:
版本1:基础实现(依赖typeof判断)
type FieldTwoType<T extends number | string> = T extends number ? Bar[] : Baz[]; class Foo<T extends number | string> { private fieldOne: T; private fieldTwo: FieldTwoType<T>; constructor(paramOne: T, paramTwo: FieldTwoType<T>) { this.fieldOne = paramOne; this.fieldTwo = paramTwo; } // 通用逻辑:基于Bar/Baz的公共属性操作 generalFunction() { console.log(this.fieldTwo.map(item => item.foo).join(',')); this.specificFunction(); } // 特定逻辑:通过typeof自动收窄类型 specificFunction() { if (typeof this.fieldOne === 'number') { // 编译器自动推断fieldTwo为Bar[] console.log(this.fieldTwo.map(item => item.bar).join(',')); } else { // 编译器自动推断fieldTwo为Baz[] console.log(this.fieldTwo.map(item => item.baz).join(',')); } } } // 合法实例化 const fooBar = new Foo(123, [{ foo: "bar1", bar: 1 }, { foo: "bar2", bar: 2 }]); const fooBaz = new Foo("abc", [{ foo: "baz1", baz: 3 }, { foo: "baz2", baz: 4 }]); // 非法实例化:编译器报错(不符合类型约束) // const invalidFoo = new Foo(123, [{ foo: "baz", baz: 5 }]);
版本2:自定义类型守卫(更清晰的类型收窄)
如果需要更明确的类型判断,可以添加类内类型守卫方法,完全避免类型断言:
type FieldTwoType<T extends number | string> = T extends number ? Bar[] : Baz[]; class Foo<T extends number | string> { private fieldOne: T; private fieldTwo: FieldTwoType<T>; constructor(paramOne: T, paramTwo: FieldTwoType<T>) { this.fieldOne = paramOne; this.fieldTwo = paramTwo; } generalFunction() { console.log(this.fieldTwo.map(item => item.foo).join(',')); this.specificFunction(); } // 自定义类型守卫:判断当前实例是否为number+Bar[]组合 isBarInstance(): this is Foo<number> { return typeof this.fieldOne === 'number'; } specificFunction() { if (this.isBarInstance()) { // 无需断言,fieldTwo自动为Bar[] console.log(this.fieldTwo.map(item => item.bar).join(',')); } else { // fieldTwo自动为Baz[] console.log(this.fieldTwo.map(item => item.baz).join(',')); } } }
方案优势
- 严格类型约束:编译器自动校验构造函数参数的类型匹配,杜绝非法组合
- 实现简洁:单个类即可完成需求,无需子类或额外参数包装
- 自然类型收窄:通过
typeof或自定义类型守卫,编译器自动推导fieldTwo的具体类型,无需手动断言
内容的提问来源于stack exchange,提问作者eoinmullan
相关产品推荐
相关产品推荐

