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

Get instance of a class in a discriminated union with its specific type, rather than the union type

有没有一个很好的方法可以让一个函数根据你传递给它的共享 属性 的值创建一个 class 的新实例,并让它 return 创建实例的具体类型,而不是联合类型?使用形状稍微调整打字稿文档示例:

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

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

您不必创建映射;你可以从你的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

return 类型使用 Extract<T, U>,一种 built-in 条件类型来过滤联合 T 以仅允许可分配给另一个类型 U 的成分。

请注意,我在 createShape() 中使用了 single-call-signature overload,因为编译器无法真正验证 switch 语句总是 return s 与未解析的条件类型 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]();
}

Link to code

因为我正在使用辅助函数 verifyShapeConstructors() 来确保我不会弄乱 constructor-holding 对象键。我正在利用这样一个事实,即编译器知道像 Square 这样的 class 构造函数有一个 "prototype" 属性 实例类型......所以它可以使用索引访问 属性 以检查构造函数的实例类型。 (built-in 条件类型 InstanceType<C> 的行为类似,但编译器无法像推理索引访问那样推理条件类型)。

所有这些归结为一个事实,即 createShape() 现在是编译器验证为正确的 one-line 泛型函数。

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