首页 > 解决方案 > TypeScript 中具有条件类型返回类型的函数的最小实现

问题描述

TypeScript 手册提供了以下使用条件类型而不是函数重载的示例:

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html


interface IdLabel {
  id: number /* some fields */;
}

interface NameLabel {
  name: string /* other fields */;
}

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;


function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}

let a = createLabel("typescript");
//  ^ = let a: NameLabel

let b = createLabel(2.8);
//  ^ = let b: IdLabel

let c = createLabel(Math.random() ? "hello" : 42);
//  ^ = let c: NameLabel | IdLabel

但是,手册并未提供该功能的实际实现。我尝试按如下方式实现该功能(操场):

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  if (typeof idOrName === 'number') {
    return { id: idOrName };
  }
  return { name: idOrName };
          // ^ Type '{ name: T; }' is not assignable to type 'NameOrId<T>'.(2322)
}

这会导致编译器错误,因此显然 TypeScript 的类型缩小无法解决此问题。我需要哪些额外信息来为编译器提供有效实现?提前谢谢了。

标签: typescripttypesconditional-statements

解决方案


这是目前 TypeScript 的限制。请参阅microsoft/TypeScript#33912以获取改进此功能的功能请求。

编译器在推理未指定的泛型类型时并不是特别擅长,尤其是当类型是条件类型时。像NameOrId<T>, whereT不是特定类型的类型(例如T的实现中的泛型类型参数createLabel())对编译器或多或少是不透明的。它推迟评估类型,直到指定了它所依赖的泛型类型参数。在那之前,它无法真正验证任何特定类型是否可分配给它。

所以你得到的只是这些错误。目前只有解决方法。


当编译器无法验证某个表达式是否可以分配给某个类型,但您确定这一点时,您可以使用类型断言告诉编译器您的确定性。只要编译器认为并且足够相关,类型断言将允许您将x编译器视为类型X as的表达式视为类型Y( ) 的表达式。如果它不这么认为,您通常可以使用与and相关的中间断言来强制这种情况发生(例如or .x as YXYXYx as any as Yx as unknown as Y

createLabel()可能如下所示:

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  if (typeof idOrName === 'number') return { id: idOrName as number } as NameOrId<T>;
  return { name: idOrName as string } as NameOrId<T>;
}

另一种解决方法是继续使用重载,它允许将函数的调用签名(可能是其中的多个)与其实现签名分开。编译器在根据调用签名检查实现签名时相当松散,这往往会给开发人员与类型断言类似的余地,而不必到处写as Y。不过,它在类型安全方面也有同样的问题,所以当你这样做时,你实际上是从编译器那里承担了一些验证安全性的责任。

在您的情况下,我们只有一个调用签名,即返回类型为条件类型的通用调用签名。对于实现签名,我们可以扩大T到相关的约束,string | number

function createLabel<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabel(idOrName: number | string): NameOrId<number | string> {
  if (typeof idOrName === 'number') return { id: idOrName };
  return { name: idOrName };
}

编译器很高兴,因为它看到实现返回类型为IdLabel | NameLabel,虽然不能验证分配给NameOrId<T>,但足够相似,可以认为重载是兼容的。


只是为了强调这一点:这是一种解决方法。编译器无法验证您到底在做什么。你会被阻止做一些完全疯狂的事情,比如

function createLabelBonkers<T extends number | string>(idOrName: T): NameOrId<T> {
  return new Date() as NameOrId<T>; // error
  // Conversion of type 'Date' to type 'NameOrId<T>' may be a mistake
}

function createLabelBonkers2<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabelBonkers2(idOrName: number | string): NameOrId<number | string> {
  return new Date(); // error, not assignable to NameLabel | IdLabel
}

但是如果你在涉及的类型与正确的类型相关的地方犯了错误,编译器不会也不能警告你:

function createLabelBad<T extends number | string>(idOrName: T): NameOrId<T> {
  return (Math.random() < 0.5 ? { id: 123 } : { name: "abc" }) as NameOrId<T>
}

function createLabelBad2<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabelBad2(idOrName: number | string): NameOrId<number | string> {
  return Math.random() < 0.5 ? { id: 123 } : { name: "abc" };
}

它没有看到 和 的实现之间的createLabel()区别createLabelBad()。在这两种情况下,它都无法检测到实现是否正确;这样做的负担在你身上,所以要小心。

Playground 代码链接


推荐阅读