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

IBO*_*ED2 1 discriminated-union typescript

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

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'
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

jca*_*alz 5

您不必创建映射;你可以从你的类型中提取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
Run Code Online (Sandbox Code Playgroud)

代码链接

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

请注意,我用一个单一的呼叫签名超载createShape(),因为编译器是不是真的能够确认switch语句总是返回未解决的条件类型相匹配的东西Extract<Shape, { kind: K}>

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


更新:你没有要求这个,但如果我正在编写一个函数,比如createShape()我可能会存储一个持有构造函数的对象,并使用索引访问让编译器验证我在里面的类型安全性createShape()

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]();
}
Run Code Online (Sandbox Code Playgroud)

代码链接

因为我使用了一个辅助函数verifyShapeConstructors()来确保我不会弄乱持有构造函数的对象键。我正在利用这样一个事实,即编译器知道类构造函数Square具有"prototype"实例类型的属性……因此它可以使用该属性的索引访问来检查构造函数的实例类型。(内置条件类型的InstanceType<C>行为类似,但编译器不能像索引访问那样对条件类型进行推理)。

所有这一切都归结为这样一个事实,createShape()即现在是编译器验证为正确的单行通用函数。

正如我所说,你没有要求这个,但它可能会引起一些兴趣。

  • 非常酷,谢谢(我知道它说不要使用这样的评论,但不感谢有人花时间帮助你感觉很粗鲁):) (2认同)