首页 > 解决方案 > 使用泛型类型和联合类型

问题描述

有了这段代码,我想很清楚我想要实现什么,有什么想法可以尝试按预期获取代码吗?

interface Person {
  name: "Peter" | "Robert";
  age: number;
  isPerson: true
}

interface Animal {
  name: "Jerry" | "Tom";
  age: number;
  isPerson: false

}

type LivingBeing = Person | Animal

function getRandomName<T extends LivingBeing>(isPerson: T["isPerson"]): T["name"] {
  const names: T["name"][] = isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
  return names[Math.floor(Math.random()*names.length)];
}

const person: "Peter" | "Robert" = getRandomName(true) // Only valid values are "Peter" | "Robert". Not "Peter" | "Robert" | "Jerry" | "Tom" as the compiler says. Check it in the Playground TS

标签: typescripttypescript-generics

解决方案


问题:没有推理站点T

您遇到的问题是在以下调用签名中,

declare function getRandomName<T extends LivingBeing>(isPerson: T["isPerson"]): T["name"];

泛型类型参数没有一个好的推理站点T。您希望编译器会查看isPerson类型T["isPerson"],并使用它来推断T。不幸的是,它没有;表单的索引访问类型T[K]不能用于推断TK

曾有一次来自 TS 团队成员之一的拉取请求,microsoft/TypeScript#20126,这将增加对此的支持。唉,它从未合并到主分支中,也不是语言的一部分。从问题历史中尚不清楚为什么。那好吧。

没有泛型类型参数的推理站点T,并且如果您不手动指定它

getRandomName<Person>(true);

那么推理将失败,编译器会退回到它的约束

getRandomName(true);
// function getRandomName<LivingBeing>(
//   isPerson: boolean): "Peter" | "Robert" | "Jerry" | "Tom"

由于T被限制为LivingBeing,因此推断为LivingBeing,并且LivingBeing["isPerson"]boolean,并且LivingBeing["name"]是四个名称的并集。你已经失去了和输出类型之间的任何true联系false。哎呀。


解决方案:提供一个推理站点T

如果您想调用以getRandomName()正确推断类型参数,则需要为编译器提供一个很好的简单推断站点。最简单和最直接的方法是使类型参数与传入的函数参数的类型完全一致。您正在传入isPerson,所以让我们将 的类型设置isPerson为我们将调用的类型参数P。我们必须限制P的有效类型isPerson,即LivingBeing["isPerson"](也称为boolean,但我们将保持这样的状态)。所以调用签名看起来像这样:

declare function getRandomName<P extends LivingBeing["isPerson"]>(
  isPerson: P
): ???;

代替???我们需要计算所需的输出类型。首先,给定,工会的P哪个成员是合适的?Person这本质上是在类型级别上区分一个有区别的联合。我们可以使用条件类型来做到这一点:具体来说,实用Extract<T, U>程序类型

// this is just to demonstrate; we don't need to use the type alias
type DiscriminateLivingBeing<P extends LivingBeing["isPerson"]> =
  Extract<LivingBeing, { isPerson: P }>;

type TrueType = DiscriminateLivingBeing<true> // Person
type FalseType = DiscriminateLivingBeing<false> // Animal

一旦我们有了它,我们只需要查找name属性。所以这是新的调用签名:

declare function getRandomName<P extends LivingBeing["isPerson"]>(
  isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];

我们可以测试它:

const person = getRandomName(true)
// const person: "Peter" | "Robert"

看起来不错。我们完成了,对吧?呃,不完全是:


一个皱纹:通用条件呼叫签名难以实施

但是,如果您尝试getRandomName()完全按照以前的方式实现,则会遇到问题:

function getRandomName<P extends LivingBeing["isPerson"]>(
  isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"] {
  const names: Extract<LivingBeing, { isPerson: P }>["name"][] = // error!
    isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"] 
  return names[Math.floor(Math.random() * names.length)]; // error!
}

在函数的实现内部,P是一个未解析或未指定的泛型类型参数。并且类型Extract<LivingBeing, {isPerson: P}>["name"][]是依赖于它的条件类型。当一个条件类型依赖于一个未解析的类型参数时,编译器基本上放弃了试图理解是否有任何特定的值可以分配给它。它完全推迟评估它。因此,尽管人类可以通过不同的可能性来P说服自己isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]可以分配给Extract<LivingBeing, {isPerson: P}>["name"][],但编译器却无法做到这一点。所以它抱怨。

在microsoft/TypeScript#33912有一个功能请求,它要求对实现调用签名涉及通用条件类型的函数提供更好的支持。但是现在,您必须使用类型断言之类的方法来解决它,以使编译器警告静音。

通常在这种情况下,我使函数成为具有单个调用签名的重载。对重载函数语句实现的检查更加松散,因此我将调用签名设为完全正确的签名:

// call signature
function getRandomName<P extends LivingBeing["isPerson"]>(
  isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];

然后函数实现输入和输出类型被扩大到足以使实现工作:

// implementation
function getRandomName(isPerson: LivingBeing["isPerson"]): LivingBeing["name"] {
  const names = isPerson ? ["Peter", "Robert"] as const : ["Jerry", "Tom"] as const
  return names[Math.floor(Math.random() * names.length)];
}

我使用了const断言,因此编译器将跟踪数组中字符串的字符串文字类型。但除此之外,它是一个简单的实现。如果你真的想要,你可以进一步放松它,甚至免除它:

// implementation
function getRandomName(isPerson: boolean): string {
  const names = isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
  return names[Math.floor(Math.random() * names.length)];
}

无论哪种方式,现在都没有错误,您可以按照您想要的方式调用该函数:

const person = getRandomName(true)
// const person: "Peter" | "Robert"
const animal = getRandomName(false)
// const animal: "Jerry" | "Tom"

Playground 代码链接


推荐阅读