在TypeScript中,有没有一种方法可以将“extends T”用作类型?

3

问题

我想定义一个带有宠物的人,这个宠物是一些扩展自Animal的东西,而不仅仅是一个Animal

interface Person {
  name: string
  pet: extends Animal  //I'm looking for something that would work here
}

我尝试过的

假设我们有以下这些接口:

interface Animal {
  name: string
}

interface Fish extends Animal {
  swim: () => void
}

interface Bird extends Animal {
  fly: () => void
}
1. 使用父类型

我知道可以这样做:

interface Person {
  name: string
  pet: Animal // Not what I want, because it needs to be only things that extend Animal
}

但这将会妨碍推理。因为如果pet不是Fish,那么它应该是Bird。但是这样它同样可以是一个普通的Animal

2. 联合类型

为了只使用扩展Animal的类型,我可以使用联合类型自己选择它们:

interface Person {
  name: string
  pet: Fish | Bird // Not what I want, because I would need to update it with new animals
}

但是如果我想创建更多的动物类型,每次都必须更新Person

例如,如果我添加:

interface Mole extends Animal {
  dig: () => void
}

我希望能够自动将Mole作为宠物添加到Person中,而不需要显式地将其添加,只需将pet: Fish | Bird更改为pet: Fish | Bird | Mole

摘要

我正在寻找一种方法,可以接受任何扩展Animal的东西,但不包括父级Animal本身,而无需每次出现新动物时都更新它。在TypeScript中如何实现?

注意

我不需要Animal作为一个接口存在,但如果它可以保持接口形式,那就太好了。

2个回答

3
这就是泛型发挥作用的地方:
interface Person<T extends Animal> {
  name: string
  pet: T
}

let personWithBirdPet: Person<Bird>;
let personWithFishPet: Person<Fish>;
let personWithMolePet: Person<Mole>;

泛型有点像“类型参数”,就像您可以为函数定义“值参数”(function(foo, bar) {})一样,我们可以为大多数类型结构(包括函数function<A, B>(foo: A, bar: B) {}和接口,如上所示)定义称为泛型的“类型参数”。

在使用泛型时,我们可以使用extends关键字对泛型应用约束。例如,function<A extends number>(value: A) {}几乎与编写function(value: number) {}相同。使用泛型版本更加强大,但也更加冗长。在这个简单的function示例中,使用泛型会过度设计。然而,在您的interface情况下,使用泛型正是您所需要的。

记住鸭子类型

由于TypeScript是鸭子类型的(如果它走起来像鸭子,说起话来像鸭子,那么它就是鸭子)。您可能会遇到一些意外行为,需要注意。

例如,您可以将一个具有通用动物作为宠物的人:

let personWithGenericPet: Person<Animal>;

..或者你可以拥有一个带着宠物的人,这个宠物不是“动物”,但看起来像动物:

let personWithStrangePet: Person<{ name: 'bob' }>;

说到这个,一个Person看起来像动物,所以你可以拥有一个人作为宠物:

let personWithPersonPet: Person<Person<Animal>>;

这些问题不是由泛型引起的,而是与 TypeScript 的工作原理密切相关。

playground


因此,我现在需要在以前使用Person作为类型的所有地方提供一个类型参数。但我想我可以在那里(滥用)使用Person<Animal>... 另外,我正在权衡之前和未来的Person键入所需的额外工作,还是只是使用pet: Fish | Bird | Mole。在我的代码库中,只要添加更多的Animal时只需要更新Person,这似乎更实用一些。 - CodingNeeL
1
如果你打算到处插入 Person<Animal>,那么可以为泛型添加默认值(interface Person<T extends Animal = Animal>),然后简单地使用 Person 就行了。这样就不需要到处更新了。 - Olian04
我刚意识到这会破坏类型推断,对吧?在此示例中,您可以在else分支中执行pet.fly(),但如果我默认为Animal,那么即使我们知道它不是Fish,我也需要先确定它实际上是Bird - CodingNeeL
1
@NeeL 是的,那会破坏推断。如果您需要推断能够即插即用,则需要通过每个依赖于“人员”的函数向下发送通用(我的回答中的 T)变量。例如,请查看我一段时间前编写的这个随机ts-playrgound - Olian04
额...这让我更倾向于使用联合类型。谢谢你的努力,详尽的回答和回复!顺便说一句,你的短网址对我不起作用,但我理解了意思。 - CodingNeeL

1
这个能行吗?
interface Person<T extends Animal> {
    name: string;
    pet: T;
}

2
是的,谢谢!我没有想到在那个层面上使用泛型。虽然你比 @Olian04 早了5秒钟,但我接受了他们的答案,因为它提供了一个小例子和解释,对未来访问这个问题的人可能有用。 - CodingNeeL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接