如何在 TypeScript 的泛型函​​数中使用判别式?

How to use discriminant in generic functions in TypeScript?

在使用带泛型的可区分联合时,我需要编写一些冗余代码,这也可能对类型安全造成危害。

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;

function<T extends Shape> fetch(type: Shape['kind'], id: string): T {
  // fetch data from database via id
}

问题是函数 fetch 中有多余的类型说明,我可以使用 Shape['kind'] 将类型限制为 'square' | 'rectangle' | 'circle',但像 fetch<Square>('circle', 'some-id') 这样的调用将仍然编译。如何解决这个问题呢?有没有办法像以下版本之一一样定义函数?

  1. fetch<T extends Shape>(type: T['kind'], id: string):T
  2. fetch<T extends Shape['kind']>(type: T, id: string): SomeMagic<T>SomeMagic<T> 帮助编译器找到合适的类型,比如 SomeMagic<'square'> 在编译时推断出 Square?

我推荐您的 "version 2" 方法如下:

declare function fetch<T extends Shape['kind']>(
  type: T, 
  id: string
): Extract<Shape, {kind: T}>;
fetch<'square'>('circle', 'some-id'); //error
const shape = fetch('rectangle', 'some-id'); // shape is a Rectangle

"magic" 是 Extract 定义的 in the standard library, which is a conditional type 类型函数,它从联合中提取匹配元素。

可以采用您的"version 1"方法,但我不推荐它:

declare function fetch<T extends Shape>(type: T['kind'], id: string): T;
fetch<Square>('circle', 'some-id'); //error
const shape = fetch('rectangle', 'some-id'); // shape is a Shape

请注意 type 参数是 T['kind'],而不是 Shape['kind']。如前所述,这解决了您的问题,但是如果您允许编译器从参数中推断出 T,它最终只会推断出 Shape,因为 T['kind'] 不是一个很好的推断点T.

无论如何,希望对您有所帮助。祝你好运。

我们需要处理的第一件事是将形状与它们各自的种类绑定。您的原始解决方案:

/**
 * Bad — doesn't bind the `Shape` with its `kind`. It allows calling `fetch<Square>('circle', 'foo')`.
 */
declare function fetch<T extends Shape>(type: Shape['kind'], id: string): T;

相反,告诉 TypeScript type 需要的不仅仅是任何一种,而是我们当前正在考虑的那种形状。

/**
 * Better: the `kind` is bound with its corresponding `Shape`. The downside: The exact return type is not inferred.
 */
declare function fetch<T extends Shape>(type: T['kind'], id: string): T;

const oups = fetch<Square>('circle', 'foo'); // $ExpectError
const shape = fetch('circle', 'foo');        // $ExpectType Shape

这样更好,但是 return 类型只是一个 Shape。我们可以通过为您的函数指定重载来做得更好:

/**
 * Using overloads can help you determine the exact return type.
 */
declare function fetch(type: 'circle', id: string): Circle;
declare function fetch(type: 'square', id: string): Square;
declare function fetch(type: 'rectangle', id: string): Rectangle;

const circle = fetch('circle', 'foo'); // $ExpectType Circle

这将为您提供准确的 return 类型,但代价是必须编写更多代码。有人可能还会争辩说存在一些冗余 — 形状与其种类之间的联系已经封装在其界面中,因此以重载的形式重复它似乎并不完美。