【问题标题】:Inheritance and static class members - whats the type of the constructor/class before it's instantiated?继承和静态类成员 - 构造函数/类在实例化之前的类型是什么?
【发布时间】:2018-01-21 13:38:13
【问题描述】:

完全人为的示例传入 - 它是解释正在发生的事情的最简单方法。

我认为我可能在错误的级别上进行了抽象完全,请告诉我!

我有一个许多其他类实现的抽象类。扩展抽象类的每个类都有多个可以使用的可能标识符,因此我希望每个类都用静态成员封装它们的标识符,而不是创建一个包含许多可能值的映射,这些值指向一个类。

我有一个类集合(DogCatHamster),它们实现了一些抽象类Pet。这些包含在实用程序类PetIdentifier 中。这个抽象类Pet 有一个带有签名isNameOk(name: string) 的静态方法。然后扩展Pet 类的每个类都有一个AcceptableNames 的静态列表。在静态方法isNameOk 中,我希望能够检查该名称是否在该类的可接受名称列表中。然后我希望能够传递给PetIdentifier,一个名称并取回我可以实例化的可能宠物类的集合。注意:在完成过滤之前,我不想实例化。

class PetIdentifier {
  constructor (private pets: Array<The Pet Class/Constructor Type> = [Dog, Cat, Hamster]) {}
  getPossiblePets (name: string) : Array<The Pet Class/Constructor Type> {
    return this.pets.filter((pet) => pet.isNameOk(name))
  }
}

abstract class Pet {
  private static names: Array<string>
  constructor () {}
  static isNameOk (name: string) : boolean {
    return this.names.indexOf(name) > -1
  }
}

class Dog extends Pet {
  names = ['rex', 'fluffy', 'odin']
}
class Cat extends Pet {
  names = ['fluffy', 'garfield', 'socks']
}
class Hamster extends Pet {
  names = ['thor', 'odin', 'loki']
}

const petIdentifier = new PetIdentifier()
const possiblePetTypes = petIdentifier.getPossiblePets('fluffy')
// instantiate them from here onward and use them
possiblePetTypes.forEach() 

【问题讨论】:

  • 如果您有兴趣,我会更新我对这个问题的回答,并提供更多信息。

标签: class typescript inheritance types static


【解决方案1】:

在我们开始之前,您可能应该为DogCatHamster 添加方法或一些区别特征(例如bark() 用于Dogmeow() 用于Cat,以及。 .. uh... spin() for Hamster),以便 TypeScript 编译器可以区分它们 structurally


您可以使用typeof Pet 引用Pet 类的静态部分,因此上面的代码基本上将使用typeof Pet 代替The Pet Class/Constructor Type 进行编译。不幸的是,您不能在 forEach() 调用中实例化它,因为 Pet 类是抽象的:

possiblePetTypes.forEach(x => new x()); // error, can't do that

所以下一步是描述Pet的可构造子类的静态方面:

type PetConstructor = {
  new(): Pet;
  isNameOk(name: string): boolean;
}

现在您将The Pet Class/Constructor Type 替换为PetConstructor,您会发现可以实例化它:

possiblePetTypes.forEach(x => new x()); // works fine

完成了,对吧?也许会。你只知道possiblePetTypes 产生了一个Pet 子类的数组,但你不知道它们可能是哪些子类。


您可以像这样使PetConstructorPetIdentifier 通用:

type PetConstructor<P extends Pet> = {
  new(): P;
  isNameOk(name: string): boolean;
}

class PetIdentifier<PC extends PetConstructor<{}>> {
  constructor (private pets: Array<PC>) {}
  getPossiblePets (name: string) : Array<PC> {
    return this.pets.filter((pet) => pet.isNameOk(name))
  }
}

(请注意,我在PetIdentifier 的构造函数中删除了[Dog, Cat, Hamster] 的默认值,因为我们可能希望泛型PCPet 的其他类型。如果需要,我们可以修复它) .

const petIdentifier = new PetIdentifier([Dog, Cat, Hamster])
const possiblePetTypes = petIdentifier.getPossiblePets('fluffy')
possiblePetTypes.forEach(x => new x()); // created Pet is Dog | Cat | Hamster

现在您知道possiblePetTypesDogCatHamster。完成了,对吧?也许吧,但听起来你想在编译时知道possiblePetTypes 包含DogCat Hamster,因为'fluffy' 不是不是Hamster 的可能名称。


好吧,我不知道有什么好方法可以做到这一点。? TypeScript 的控制流分析还不够复杂,无法意识到possiblePetTypes 更窄,静态类型系统也不够完善- 特色,因为您需要轻松表达它。我想使用conditional mapped types,以便TypeScript 可以通过检查Dog.namesCat.namesHamster.names 的类型来开始构建Dog | Cat。但它似乎还没有。

我现在能做的就这么多了。希望能帮助到你;祝你好运!


更新

自从我写这篇文章以来,我一直在玩弄类型系统,并找到了一些可以用来推断正确类型的东西。您可能感兴趣,或者您可能认为这有点矫枉过正。这是新代码:

首先,一些辅助函数和类型:

type Lit = string | number | boolean | undefined | null | void | {};
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit, K extends Lit, L extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L): [A, B, C, D, E, F, G, H, I, J, K, L];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit, K extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K): [A, B, C, D, E, F, G, H, I, J, K];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J): [A, B, C, D, E, F, G, H, I, J];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I): [A, B, C, D, E, F, G, H, I];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): [A, B, C, D, E, F, G, H];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G): [A, B, C, D, E, F, G];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F): [A, B, C, D, E, F];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit>(a: A, b: B, c: C, d: D, e: E): [A, B, C, D, E];
function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit>(a: A, b: B, c: C, d: D): [A, B, C, D];
function tuple<A extends Lit, B extends Lit, C extends Lit>(a: A, b: B, c: C): [A, B, C];
function tuple<A extends Lit, B extends Lit>(a: A, b: B): [A, B];
function tuple<A extends Lit>(a: A): [A];
function tuple(...args: any[]): any[] {
  return args;
}

type Constructor<T> = {
  new(...args: any[]): T;
  readonly prototype: T;
}

type Diff<T extends string, U extends string> = ({[K in T]: K} &
  {[K in U]: never} & { [K: string]: never })[T];

现在是繁重的工作。我将所有静态方法/属性更改为实例方法/属性。这可能不是您想要的,但是以这种方式操作类型要容易得多。如果你喜欢这种方法,但还需要静态方法,你可能可以这样做,但它会更加乏味。

abstract class Pet {
  names: string[];
  constructor() {}
  isNameOk(name: string): boolean {
    return this.names.indexOf(name) > -1;
  }
  ["constructor"]: Constructor<this>;
  nameMap: Record<string, Pet> &
    Record<this['names'][number], this> &
    Record<Diff<AllPets['names'][number], this['names'][number]>, never> = null!;
    // type helper, just null at runtime
}

请注意,我添加了一个 nameMap 属性,该属性在运行时为空。 TypeScript 使用它来维护从每个可能的宠物名称到宠物类型的映射。

class PetIdentifier<P extends Pet> {
  constructor (private pets: Array<P>) {}
  getPossiblePets<N extends string>(name: N) : Array<P['nameMap'][N]> {
    return this.pets.filter((pet) => pet.isNameOk(name))
  }
}

现在getPossiblePets() 返回一个仅包含相关宠物类型的数组。

class Dog extends Pet {
  names = tuple('rex', 'fluffy', 'odin') //  use tuple for literals
  bark() { }
}
class Cat extends Pet {
  names = tuple('fluffy', 'garfield', 'socks')
  meow() { }
}
class Hamster extends Pet {
  names = tuple('thor', 'odin', 'loki')
  spin() { }
}
type AllPets = Dog | Cat | Hamster; // need an AllPets type

请注意,我使用了tuple() 辅助函数来声明names 属性;这允许names 被推断为字符串文字的元组而不是字符串数组。 TypeScript 需要字符串文字来进行映射。

还要注意我需要AllPets 类型,它是Pet 的所有已声明子类的显式联合。这在PetnameMap 属性中是必需的,因此,例如,Cat 知道名称,例如thor,并且当被问及是否可以命名 Cat 时可以回答“否” thor。 (不,没有办法设置它,以便Cat 可以对任何它不认识的名字说“不”)。

最后,结果:

const petIdentifier = new PetIdentifier([new Dog(), new Cat(), new Hamster()]);
const possiblePetTypes = petIdentifier.getPossiblePets('fluffy')
// possible pet types is (Dog | Cat)[], no Hamster! ?

了解possiblePetTypes 如何不再包含Hamster

如果你给它一个完全随机的名字,比如:

const impossiblePetTypes = petIdentifier.getPossiblePets('galactus');
// impossiblePetTypes is Pet[]

它将返回通用的Pet[] 类型。不,我无法让它返回类似never[] 的内容,抱歉。

好的,这种方法很有效,而且非常疯狂,可能任何人都无法维护。我想我会建议在设计时放弃知道Pet 来自getPossiblePets() 的类型,并接受这样一个事实,即在运行时更好地处理这种事情。如果你传入一个在设计时值未知的字符串:

declare const someName: string;
const possiblePetTypes = petIdentifier.getPossiblePets(someName); // Pet[]

那么你从所有这种类型的杂耍中获得零收益。嗯是的。将上述内容视为一种有趣的消遣,但我不愿意尝试任何你关心部署到某种生产系统的东西。再次祝你好运!

【讨论】:

    猜你喜欢
    • 2013-05-07
    • 1970-01-01
    • 1970-01-01
    • 2015-03-21
    • 2020-12-22
    • 2021-04-28
    • 1970-01-01
    • 1970-01-01
    • 2012-03-30
    相关资源
    最近更新 更多