TypeScript 中具有条件类型返回类型的函数的最小实现

Tre*_*anz 8 types conditional-statements 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

Run Code Online (Sandbox Code Playgroud)

然而,手册没有提供该功能的实际实现。我尝试按如下方式实现该功能(游乐场):

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)
}
Run Code Online (Sandbox Code Playgroud)

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

jca*_*alz 10

这是目前 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>;
}
Run Code Online (Sandbox Code Playgroud)

另一种解决方法是继续使用重载,它允许函数的调用签名(可能是多个)与其实现签名之间的分离。编译器在根据调用签名检查实现签名时相当宽松,这往往会给开发人员提供与类型断言类似的回旋余地,而不必到处编写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 };
}
Run Code Online (Sandbox Code Playgroud)

编译器很高兴,因为它将实现返回类型视为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
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您在所涉及的类型与正确类型相关的情况下犯了错误,编译器不会也无法警告您:

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" };
}
Run Code Online (Sandbox Code Playgroud)

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

Playground 代码链接