在我们开始之前,您可能应该为Dog、Cat 和Hamster 添加方法或一些区别特征(例如bark() 用于Dog,meow() 用于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 子类的数组,但你不知道它们可能是哪些子类。
您可以像这样使PetConstructor 和PetIdentifier 通用:
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] 的默认值,因为我们可能希望泛型PC 是Pet 的其他类型。如果需要,我们可以修复它) .
const petIdentifier = new PetIdentifier([Dog, Cat, Hamster])
const possiblePetTypes = petIdentifier.getPossiblePets('fluffy')
possiblePetTypes.forEach(x => new x()); // created Pet is Dog | Cat | Hamster
现在您知道possiblePetTypes 是Dog 或Cat 或Hamster。完成了,对吧?也许吧,但听起来你想在编译时知道possiblePetTypes 包含Dog 或Cat 但不 Hamster,因为'fluffy' 不是不是Hamster 的可能名称。
好吧,我不知道有什么好方法可以做到这一点。? TypeScript 的控制流分析还不够复杂,无法意识到possiblePetTypes 更窄,静态类型系统也不够完善- 特色,因为您需要轻松表达它。我想使用conditional mapped types,以便TypeScript 可以通过检查Dog.names、Cat.names 和Hamster.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 的所有已声明子类的显式联合。这在Pet 的nameMap 属性中是必需的,因此,例如,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[]
那么你从所有这种类型的杂耍中获得零收益。嗯是的。将上述内容视为一种有趣的消遣,但我不愿意尝试任何你关心部署到某种生产系统的东西。再次祝你好运!