在打字稿中使用带柯里化的文字时如何防止缩小泛型类型?

How to prevent narrowing generic types while using literals with currying in typescript?

我在为 Sanctuary(用于函数式编程的 js 库)设计类型时遇到问题。我想创建 Ord 类型,代表具有自然顺序的任何值。 Ord 是:

  1. 一些内置原始类型:numberstringDateboolean
  2. 实现所需接口(compareTo 方法)的任何用户类型
  3. Ord个数组

为了表达它,我使用了(这对我来说很自然)联合类型(在使重复类型成为可能方面有一些变通方法),例如:

type Ord = boolean | Date | number | string | Comparable<any> | OrderedArray

interface OrderedArray extends Array<Ord> {}

(我提到的任何代码都可以在 typescript playground 中找到)。

然后,创建类型定义,例如compare 函数,我使用了带有类型约束的泛型类型,即类似于(注意它是柯里化的):

function compare<T extends Ord>(a: T): (b: T) => number

不幸的是,在使用这种带有文字参数的方法时,例如(compare(2)(3),这是完全有效的代码),打字稿错误,'3' is not assignable to '2'。似乎在提供第一个参数的情况下,typescript 将类型参数缩小到 2(仅包含 2 值的文字类型),而它应该只缩小到 number。有什么办法可以阻止他这样做吗?或者任何其他工作方法?

如果您简化 Ord 的定义,所有测试都会通过。

interface Comparable<T> {
  compareTo(other: T): number
}

type Ord<T> = T | Comparable<T> | Array<T>

const compare = <T extends Ord<any>>(x: T) => (y: T) => 0;

TypeScript 游乐场link.

不过,我不确定这是否符合图书馆的所有其他要求。

我最终弄明白了(至少在某种程度上有效,不一定是最好的)。事实证明,为所有具有文字表示的类型添加显式重载,因此可以缩小,修复它,如:

function compare(x: number): (y: number) => number;
function compare(y: string): (y: string) => number;
function compare(x: boolean): (y: boolean) => number;

// and generic overload
function compare<T extends Ord>(x: T): (y: T) => number;
function compare<T extends Ord>(x: T): (y: T) => number {
  ...
};

我认为这些显式重载优先于签名解析,因此我的问题消失了,例如:

compare(2)(3) // resolves to compare(x: number)(y: number)

解决方案可在 typescript playground 获得。

如果编译器doesn't do it,您也可以使用类型系统自己进行文字扩展(例如这里的情况,其中 T 具有上下文类型,包括 stringnumber,和 boolean):

type Widen<T> = 
  T extends string ? string : 
  T extends number ? number : 
  T extends boolean ? boolean : 
  T;

const compare = <T extends Ord>(x: T) => (y: Widen<T>) => 0;

所以 x 会保持狭窄,但 y 会变宽。这将允许以下调用:

compare(1)(3);  // <1>(x: 1) => (y: number) => number

希望对您有所帮助;祝你好运!

Link to code


更新:如果您真的希望能够选择性地保持 T 狭窄,您可以执行以下复杂的操作:

type OptionallyWiden<X, T> = 
   [X] extends [never] ? 
     T extends string ? string : 
     T extends number ? number : 
     T extends boolean ? boolean : 
     T
  : T;

const compare = <X extends Ord = never, T extends Ord = X>(x: T) => (
  y: OptionallyWiden<X, T>
) => 0;

这给了你这个行为:

compare(1)(3); // okay
// const compare: <never, 1>(x: 1) => (y: number) => number

compare<1 | 2>(1)(3); // error now
//  ------------> ~
// 3 is not assignable to 1 | 2
// const compare: <1 | 2, 1 | 2>(x: 1 | 2) => (y: 1 | 2) => number

它通过使用两个类型参数来工作...第一个 X,除非手动指定,否则将默认为 never。如果Xnever,则T的推断值会加宽。如果 X 不是 never,将使用指定的值而不加宽。

这里的灵活性是否值得奇怪笨重的杂耍?我想这取决于你自己想办法,但如果我想灵活地同时拥有窄值和宽值 T,我可能会保留原来的 <T extends Ord>(x: T) => (y: T) => 0,而只是手动加宽 T如果那是我想要的。

无论如何,希望这再次对您有所帮助!

Link to code