如何使用type predicate方法缩小类私有成员的类型?
我在类FooBad中尝试用类型断言方法将私有成员private bar?的类型从string | undefined缩小为string时遇到了问题。类FooGood1和FooGood2分别通过将bar?改为public,或者用this.bar的内联检查替代hasBar()来规避问题,但我不想用这些方案——虽然FooGood2在这个最小示例里可行,但实际代码中我更希望hasBar()能配合private bar?正常工作。
相关代码示例
- 无法正常运行(
private bar?+hasBar()类型断言):
class FooBad { private bar?: string; private hasBar = (): this is { bar: string } => { return this.bar !== undefined; } printBar = () => { if (!this.hasBar()) { return; } /* 属性“bar”在类型“never”上不存在。 交集“this & { bar: string; }”被缩减为“never”,因为属性“bar”存在于多个组成部分中,且在某些部分中是私有的。 ts(2339) */ console.log(this.bar.toUpperCase()); } }
- 规避方案1(
public bar?+hasBar()类型断言):
class FooGood1 { public bar?: string; private hasBar = (): this is { bar: string } => { return this.bar !== undefined; } printBar = () => { if (!this.hasBar()) { return; } console.log(this.bar.toUpperCase()); } }
- 规避方案2(
private bar?+ 无hasBar()类型断言):
class FooGood2 { private bar?: string; printBar = () => { if (this.bar === undefined) { return; } console.log(this.bar.toUpperCase()); } }
要让FooBad正常工作,你需要调整类型断言的写法,让它引用类自身的私有成员类型,避免与公共属性类型产生冲突。以下是几种可行的修改方式:
方法1:使用Pick工具类型
修改hasBar的返回类型断言,通过Pick<FooBad, 'bar'>明确引用类的私有成员bar,再结合{ bar: string }缩小类型:
class FooBad { private bar?: string; private hasBar = (): this is Pick<FooBad, 'bar'> & { bar: string } => { return this.bar !== undefined; } printBar = () => { if (!this.hasBar()) { return; } console.log(this.bar.toUpperCase()); // 不再报错 } }
方法2:显式排除undefined类型
通过Omit和Exclude工具类型,保留类的其他属性,同时将bar的类型缩小为非undefined的字符串:
class FooBad { private bar?: string; private hasBar = (): this is Omit<FooBad, 'bar'> & { bar: Exclude<FooBad['bar'], undefined> } => { return this.bar !== undefined; } printBar = () => { if (!this.hasBar()) { return; } console.log(this.bar.toUpperCase()); // 正常工作 } }
方法3:使用类内部类型别名简化
在类内部定义一个类型别名HasBar,直接表示bar存在的状态,让类型断言更简洁:
class FooBad { private bar?: string; private type HasBar = this & { bar: string }; private hasBar = (): this is this.HasBar => { return this.bar !== undefined; } printBar = () => { if (!this.hasBar()) { return; } console.log(this.bar.toUpperCase()); // 无报错 } }
原理说明
原代码报错的核心原因是:当断言this is { bar: string }时,TypeScript会将类实例类型与这个公共对象类型做交叉。但类中的bar是私有成员,而对象类型中的bar是公共属性,两者的可见性冲突,导致交叉类型被缩减为never。通过引用类自身的私有成员类型(如Pick<FooBad, 'bar'>),TypeScript能识别出这是同一个属性,不会产生冲突,从而正确完成类型缩小。
内容的提问来源于stack exchange,提问作者Andrew Keeton

