在打字稿中使用带柯里化的文字时如何防止缩小泛型类型?
How to prevent narrowing generic types while using literals with currying in typescript?
我在为 Sanctuary(用于函数式编程的 js 库)设计类型时遇到问题。我想创建 Ord
类型,代表具有自然顺序的任何值。 Ord
是:
- 一些内置原始类型:
number
、string
、Date
、boolean
- 实现所需接口(
compareTo
方法)的任何用户类型
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
具有上下文类型,包括 string
、number
,和 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
希望对您有所帮助;祝你好运!
更新:如果您真的希望能够选择性地保持 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
。如果X
为never
,则T
的推断值会加宽。如果 X
不是 never
,将使用指定的值而不加宽。
这里的灵活性是否值得奇怪笨重的杂耍?我想这取决于你自己想办法,但如果我想灵活地同时拥有窄值和宽值 T
,我可能会保留原来的 <T extends Ord>(x: T) => (y: T) => 0
,而只是手动加宽 T
如果那是我想要的。
无论如何,希望这再次对您有所帮助!
我在为 Sanctuary(用于函数式编程的 js 库)设计类型时遇到问题。我想创建 Ord
类型,代表具有自然顺序的任何值。 Ord
是:
- 一些内置原始类型:
number
、string
、Date
、boolean
- 实现所需接口(
compareTo
方法)的任何用户类型 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
具有上下文类型,包括 string
、number
,和 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
希望对您有所帮助;祝你好运!
更新:如果您真的希望能够选择性地保持 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
。如果X
为never
,则T
的推断值会加宽。如果 X
不是 never
,将使用指定的值而不加宽。
这里的灵活性是否值得奇怪笨重的杂耍?我想这取决于你自己想办法,但如果我想灵活地同时拥有窄值和宽值 T
,我可能会保留原来的 <T extends Ord>(x: T) => (y: T) => 0
,而只是手动加宽 T
如果那是我想要的。
无论如何,希望这再次对您有所帮助!