在具有特定类型而不是联合类型的已区分联合中获取类的实例

时间:2019-06-10 15:03:07

标签: typescript discriminated-union

有没有一种很好的方法可以使函数根据您传递给它的共享属性的值创建一个类的新实例,并让该函数返回创建的实例的特定类型,而不是联合类型?略微调整一下带有形状的打字稿文档示例:

class Square {
    kind: "square";
    size: number;
}
class Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
class Circle {
    kind: "circle";
    radius: number;
}

type Kinds = "circle" | "rectangle" | "square";
type Shape = Square | Rectangle | Circle;

function createShape(kind: Kinds) {
    switch (kind) {
        case "circle":
            return new Circle();
        case "rectangle":
            return new Rectangle();
        case "square":
            return new Square();
    }
}

createShape("circle").radius; //Property 'radius' does not exist on type 'Square | Rectangle | Circle'

例如,我可以在KindsShape之间添加一个映射,并向该函数添加一些类型注释:

type Kinds = "circle" | "rectangle" | "square";
type Shape = Square | Rectangle | Circle;
type ShapeKind = { "circle": Circle, "square": Square, "rectangle": Rectangle };

function createShape<T extends Kinds>(kind: T): ShapeKind[T] {
    switch (kind) {
        case "circle":
            return new Circle();
        case "rectangle":
            return new Rectangle();
        case "square":
            return new Square();
    }
}

createShape("circle").radius; //All good now

但是必须创建此映射感觉有点讨厌。我也可以使用类型保护,但这感觉很多余,因为我可以确定在创建和返回Shape的类型。有更好的方法来解决这个问题吗?

1 个答案:

答案 0 :(得分:4)

您不必创建映射;您可以从您的Shape类型中提取

class Square {
  readonly kind = "square";
  size!: number;
}
class Rectangle {
  readonly kind = "rectangle";
  width!: number;
  height!: number;
}
class Circle {
  readonly kind = "circle";
  radius!: number;
}

type Shape = Square | Rectangle | Circle;
type Kinds = Shape["kind"]; // automatically

// return type is the consituent of Shape that matches {kind: K}
function createShape<K extends Kinds>(kind: K): Extract<Shape, { kind: K }>;
function createShape(kind: Kinds): Shape {
  switch (kind) {
    case "circle":
      return new Circle();
    case "rectangle":
      return new Rectangle();
    case "square":
      return new Square();
  }
}

createShape("circle").radius; // okay

Link to code

返回类型使用内置条件条件类型Extract<T, U>来过滤联合T,以仅允许可分配给另一类型U的成分。

请注意,我在createShape()中使用了单次调用签名overload,因为编译器实际上无法验证switch语句是否始终返回与未解决的条件匹配的内容输入Extract<Shape, { kind: K}>

希望有所帮助;祝你好运!


更新:您并没有要求这样做,但是如果我正在编写类似createShape()的函数,则可以存储一个对象持有构造函数,并使用索引访问来让编译器在{{ 1}}:

createShape()

Link to code

在这种情况下,我使用了辅助函数const verifyShapeConstructors = < T extends { [K in keyof T]: new () => { kind: K } } >( x: T ) => x; const badShapeConstuctors = verifyShapeConstructors({ square: Square, circle: Rectangle, // error! rectangle: Circle, // error! }) const shapeConstructors = verifyShapeConstructors({ square: Square, rectangle: Rectangle, circle: Circle }); type ShapeConstructors = typeof shapeConstructors; type Instance<T extends Function> = T["prototype"]; type Shape = Instance<ShapeConstructors[keyof ShapeConstructors]>; type Kinds = keyof ShapeConstructors; function createShape<K extends Kinds>(kind: K): Instance<ShapeConstructors[K]> { return new shapeConstructors[kind](); } ,以确保不会弄乱构造函数保存对象键。而且我利用了一个事实,那就是编译器知道verifyShapeConstructors()之类的类构造函数具有实例类型的Square属性...因此它可以使用该属性的索引访问来检查构造函数的实例类型。 (内置的条件类型InstanceType<C>的行为类似,但是编译器无法推理条件类型,也不能推理索引)。

所有这些可以归结为一个事实,即"prototype"现在是编译器验证为正确的单行通用函数。

正如我所说,您并没有要求这样做,但这可能会引起一些兴趣。