我有一个带有
Foo
和 fieldOne: number | string
的 TypeScript 类 fieldTwo
,当 Bar[]
是 fieldOne
时,它的类型为 number
;当 Baz[]
是 fieldOne
时,它的类型为 string
。这两个字段都作为构造函数参数提供。
类中的大多数功能都基于
Bar
和 Baz
的联合,但在一个地方我需要将类型缩小为 Bar
或 Baz
。有没有办法使用编译器强制 Foo
只能用 number
和 Bar[]
或 string
和 Baz[]
实例化。然后,在课堂上,我可以在需要时将 fieldTwo
的类型缩小为 Bar[]
或 Baz[]
吗?
我可以通过继承达到如图这里所示的效果。
interface Bar {
foo: string;
bar: number;
}
interface Baz {
foo: string;
baz: number;
}
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());
}
}
但是我有点啰嗦了。
我希望得到更简洁的条件类型,如here
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() {
// somehow narrow the type, maybe based on this.fieldOne?
// if fieldOne is number do something with Bar[];
// if fieldOne is string do something with Baz[];
}
}
但我无法让它按我需要的方式工作。
第三种方法,这里,将两个字段组合成一个对象。
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 === 'number';
}
function isStringyBazy(x: FooParams): x is {one: string, two: Baz[]} {
return typeof x === 'number';
}
但是,这比我希望的要冗长。
所以,你不会让它与泛型和条件类型一起使用,因为检查泛型值不会导致泛型类型发生变化。至少在实现类似 microsoft/TypeScript#33014 之前。
TypeScript 唯一用于检查一个属性(如
fieldOne
)并使用它来缩小 different 属性(如 fieldTwo
)的表观类型的唯一工具是当所讨论的对象是一个 discriminated union 并且第一个属性是它的判别式。不幸的是,您的 fieldOne
要么是 string
要么是 number
,这两者都不被视为判别式。您至少需要其中之一才能成为文字类型。您可以添加一个单独的属性(fieldZero
?)来达到此目的,可能使用true
/false
:
type FooParams =
{ zero: true, one: number, two: Bar[] } |
{ zero: false, one: string, two: Baz[] };
class Foo {
constructor(public params: FooParams) {}
generalFunction() {
console.log(this.params.two.map(x => x.foo).join());
}
specificFunction() {
if (this.params.zero) {
this.processBars(this.params.two);
}
else {
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());
}
}
现在
FooParams
是一个受歧视的工会。这使您不必实现“自定义类型保护函数”。这个版本的代码可能就是我处理它的方式。 Foo
只是一个容纳
FooParams
的容器。这可能不是您想要的表示数据的方式,但它效果很好。
但是你想要一个 Foo
到
成为
FooParams
,而不仅仅是 hold一个。这要棘手得多。类语句不要在 TypeScript 中创建联合类型。您可以在类型系统中表达这样的事情,但是将该类型连接到实际的类构造函数将涉及类型断言和其他解决方法,并且您可能会遇到奇怪的障碍(就像您无法轻松地子类化)
Foo
)。不过,这里有一个方法:
首先,您必须将
Foo
类重命名为 _Foo
并使其成为您想要的基础超类型:
类_Foo {
constructor(
public fieldZero: boolean,
public fieldOne: number | string,
public fieldTwo: Bar[] | Baz[]
) { }
generalFunction() {
console.log(this.fieldTwo[0].foo);
}
processBars(bars: Bar[]) {
console.log(bars.map(x => x.bar).join());
}
processBazs(bazs: Baz[]) {
console.log(bazs.map(x => x.baz).join());
}
}
然后你根据它来定义
Foo
的不同风格:
interface FooTrue extends _Foo {
fieldZero: true,
fieldOne: number,
fieldTwo: Bar[]
}
interface FooFalse extends _Foo {
fieldZero: false,
fieldOne: string,
fieldTwo: Baz[]
}
然后将类型 Foo
定义为可判别联合,将 value
Foo
定义为 _Foo
值,但断言它具有适当的类型:
type Foo = FooTrue | FooFalse;
const Foo = _Foo as {
new(fieldZero: true, fieldOne: number, fieldTwo: Bar[]): FooTrue,
new(fieldZero: false, fieldOne: string, fieldTwo: Baz[]): FooFalse
}
现在您可以使用单个 Foo
类来表现得像一个可区分的联合。 Inside
_Foo
,您只能在方法中使用 this
参数 来说服编译器该方法只会在匹配 Foo
的对象上调用,而不仅仅是 _Foo
:
specificFunction(this: Foo) {
if (this.fieldZero) {
this.processBars(this.fieldTwo);
}
else {
this.processBazs(this.fieldTwo);
}
}
这一切都可以正常工作,没有错误。这值得么?我不这么认为。如果您想要类的多态性,那么您应该按照第一种方法的工作方式拥有不同的子类。类实际上是面向继承,而不是面向单个类内的交替。或者你可以将交替作为单个类的联合属性。但这种让单个类看起来像一个联合体的混合方法实际上是在对抗语言。
Playground 代码链接