我有一个TypScript类Foo,其中包含fieldOne: number | stringfieldTwo,当fieldOnenumber时,它们将属于Bar[]类型,当fieldOnestring时,它们将属于Baz[]类型.这两个字段都作为构造函数参数提供.

类中的大部分功能都基于BarBaz的联合,但在一个地方,我需要将类型缩小为BarBaz.有没有一种方法可以使用编译器来强制Foo只能用numberBar[]stringBaz[]实例化.那么,在班上需要的时候我可以将fieldTwo的类型缩小到Bar[]Baz[]吗?

我可以通过继承来达到如here所示的效果.

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[];
  }
}

但我无法让它像我需要的那样工作.

第三种方法here将两个字段组合成一个对象.

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';
}

但同样,它比我希望的要冗长.

推荐答案

因此,您无法将其与genericsconditional types一起使用,因为无论判断多少通用value都不会导致通用type发生更改.至少在实施类似microsoft/TypeScript#33014的内容之前是这样的.


TypScript唯一用于判断一个属性(例如fieldOne)并使用该属性来缩小different属性(例如fieldTwo)的表观类型的工具是当所讨论的对象是discriminated union并且第一个属性是它的区分式时.不幸的是,您的fieldOne要么是string,要么是number,这两者都不被视为歧视. 您至少需要其中一个才能获得literal type. 您可以添加一个单独的属性(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是一个受歧视的联盟.这使您不必实施custom type guard functions. 这个版本的代码可能就是我处理它的方式.A Foo只是一个容器,其中holdsFooParams. 这可能不是您想要的数据表示方式,但效果相当好.


但您想要的是FoobeFooParams,而不仅仅是hold. 这要棘手得多. 在TypScript中,Class statements不会成为union types. 您可以在类型系统中表达这样的事情,但将该类型挂钩到实际的类构造函数将涉及type assertions和其他变通方法,并且您可能会遇到奇怪的障碍(就像您无法轻松地子分类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类来表现得像一个受歧视的unions .Inside _Foo您只能通过使用this parameter来说服编译器该方法只会在与Foo匹配而不仅仅是_Foo匹配的对象上调用:

  specificFunction(this: Foo) {
    if (this.fieldZero) {
      this.processBars(this.fieldTwo);
    }
    else {
      this.processBazs(this.fieldTwo);
    }
  }

这一切都不会出错. 值得吗?我不这么认为. 如果您想要具有类的多态性,则应该像第一种方法一样拥有不同的子类.类实际上是面向继承的,而不是面向单个类内的交替. 或者您可以将单个类别的unions property交替进行. 但这种让单一阶级看起来像一个联盟的混合方法实际上是在与语言作斗争.

Playground link to code

Typescript相关问答推荐

如何在不使用变量的情况下静态判断运算式的类型?

TypScript在对象上强制执行值类型时推断对象文字类型?

在TypeScript中使用泛型的灵活返回值类型

当使用`type B = A`时,B的类型显示为A.为什么当`type B = A时显示为`any `|用A代替?

如何通过TypeScript中的工厂函数将区分的联合映射到具体的类型(如类)?

如何将CSV/TXT文件导入到服务中?

使用Redux Saga操作通道对操作进行排序不起作用

Angular:ngx-echart 5.2.2与Angular 9集成错误:NG 8002:无法绑定到选项,因为它不是div的已知属性'

如何在使用条件类型时使用void

如何在脚本中输入具有不同数量的泛型类型变量的函数?

ANGLE NumberValueAccessor表单控件值更改订阅两次

打印脚本中正则函数和函数表达式的不同类型缩小行为

为什么我的函数arg得到类型为Never?

TypeScrip:如何缩小具有联合类型属性的对象

埃斯林特警告危险使用&as";

使用RXJS获取数据的3种不同REST API

类型';只读<;部分<;字符串|记录<;字符串、字符串|未定义&>';上不存在TS 2339错误属性.属性&q;x&q;不存在字符串

两个接口的TypeScript并集,其中一个是可选的

断言同级为true或抛出错误的Typescript函数

如何判断路由或其嵌套路由是否处于活动状态?