TypeScript:定义了解子 class 属性的父 class 的构造函数

TypeScript: Defining constructor of parent class which is aware of child class's properties

abstract class Parent {
  public foo: string;

  constructor(v: Partial<Parent>) {
    Object.assign(this, v);
  }
}

class ChildA extends Parent {
  bar: string;
}

class ChildB extends Parent {
  baz: string
}

在此设置中,

const a = new ChildA({ foo: 'foo', bar: 'bar'});

出现以下错误。

TS2345: Argument of type '{ foo: string; bar: string }' is not assignable to parameter of type 'Partial<Parent>'.
Object literal may only specify known properties, and 'bar' does not exist in type 'Partial<Parent>'.

constructor<T extends Parent>(v: Partial<T>) 也不起作用。 (TS1092:类型参数不能出现在构造函数声明中。)

我能否以类型安全的方式定义 类 Parent、ChildA 和 ChildB 的构造函数?

我认为打字稿错误是正确的

Parent只存在foo: string变量

如果你想 new ChildA 你需要在 ChildA 中定义构造来纠正从外部传递的类型

abstract class Parent {
    public foo: string;

    protected constructor(v: Partial<Parent>) {
        Object.assign(this, v);
    }
}

class ChildA extends Parent {
    bar: string;
    constructor(v: Partial<ChildA>) {
        super(v);
    }
}


const a = new ChildA({ foo: 'foo', bar: 'bar'});

您想在构造函数参数中使用 the polymorphic this type

不幸的是,这是明确不支持的;参见 microsoft/TypeScript#5449, especially this comment, and a related feature request at microsoft/TypeScript#5863. There's a relatively recent feature request to allow this in constructor parameters at microsoft/TypeScript#40451。那么现在,没有简单的方法告诉编译器您希望构造函数参数取决于“当前”class 构造函数的类型(因此被 subclasses 继承)。

如果你想要这种行为,你不会从 TypeScript 中免费获得它;你必须解决它。


多态-this 是一种“隐式”F-bounded polymorphism, meaning that you can think of this being like a generic type parameter which is constrained 自身。由于我们无法获得构造函数参数的隐式版本,也许我们可以通过向 class:

添加一个自界类型参数来 显式 来做到这一点
abstract class Parent<T extends Parent<T>> {
    public foo: string = "";

    constructor(v: Partial<T>) {
        Object.assign(this, v);
    }
}

行得通;请注意在 Parent<T> 中,类型参数 T 是如何被限制为 Parent<T> 本身的。现在我们可以使用 T 代替 this 类型。当我们声明 subclasses 时,我们需要明确指出:

class ChildA extends Parent<ChildA> {
    bar: string = "";
}

class ChildB extends Parent<ChildB> {
    baz: string = ""
}

现在您的子classes 的行为符合预期:

new ChildA({ foo: 'foo', bar: 'bar' }).bar;

您提到您可能想给 T 一个 default,这样您就可以在代码的其他部分仅提及 Parent。对于此默认值,您有多种选择,具体取决于您想要的迂腐与方便程度。最方便的是any,次之最迂腐的是Parent<any>,然后是Parent<Parent<any>>(按需要重复),最后是极端版本:

type ParentItself = Parent<ParentItself>;
abstract class Parent<T extends Parent<T> = ParentItself> { /* ... */ }

也许您甚至希望将 never 作为默认值,这可能会突出显示 Parent 在只有具体子 class 才有意义的地方的任何意外使用。您需要根据您的用例检查这些,看看哪些(如果有)是合适的。

Playground link to code