这里要明确两点:
-
TypeScript 并不(直接)支持 Existential Types。
-
什么是 Existential Types?
这涉及到(我不懂的)高阶数学和逻辑推导,所以我只说一个大概的理解:Existential Types 就是你知道一个参数是泛型参数,但又不知道具体的泛型类型时要用的类型。
下面的内容整合自一篇外文博客[1]和 StackOverflow 网站的问答[2]。
假如你有如下接口和实现:
interface Property<T> {
pget: () => T;
pset: (value: T) => void;
}
class NumberProperty implements Property<number> {
constructor(private value: number) {
}
pget() { return this.value }
pset(value: number) { this.value = value }
}
class DateProperty implements Property<Date> {
constructor(private value: Date) {
}
pget() { return this.value }
pset(value: Date) { this.value = value }
}
现在你要维护一组 Properties
,你不在乎单个 Property
的类型是什么,只要它是有效的 Property
就行,你要怎么给它声明类型?
let properties: Property<???>[];
你当然可以使用 any
或 unknown
。但使用 unknown
意味着后续调用时要进行类型转换操作,而你不一定知道要转换成哪种具体的类型;而使用 any
就失去了 TypeScript 本身的意义了。
我们想要的效果是,对于数组变量 properties
内的每个元素,它有一个泛型类型 T
,虽然我们不知 T
具体是什么,但它是 存在 的。
TypeScript 官方仓库中,有人提议用 Property<*>
这种语法来表达上述含义,但至今没被采纳和实现。
作者提到了一种利用 continuations 来实现它的方式。
/**
* 注意这里比较绕:
*
* 1. PropertyCont 是一个函数类型;
* 2. 该函数的参数是一个回调函数,回调函数的入参是 Property<T> 类型,回调函数的返回类型是 R;
* 3. PropertyCont 函数的返回类型是其入参(回调函数)的返回类型
*/
type PropertyCont = <R>(cont: <T>(prop: Property<T>) => R) => R;
/**
* 注意这里:
*
* 1. 该函数将 Property<T> 对象转换为 PropertyCont;
* 2. return 后的语句其实是就是返回一个 PropertyCont 对象(一个函数);
* 3. return 后的语句在 ts v4.2.3 中编译不通过,要改成我注释掉的 return 语句
*/
function makePropCont<T>(property: Property<T>): PropertyCont {
return <R>(cont: <T>(prop: Property<T>) => R) => cont(property);
// 正确的 return 语句
// return (cont) => cont(property);
}
let properties: PropertyCont[] = [];;
整个核心部分都在上面的注释里,一定要完整消化!接下来看看它的使用:
// ...接着上面
properties.push(
// 没问题
makePropCont(new NumberProperty(0)),
// 没问题
makePropCont(new DateProperty(new Date)),
// 可以检查出类型不匹配
// ERR:Type 'number' is not assignable to type 'string'.(2322)
makePropCont({
pget: () => 44,
pset: (badValue: string) => { }
})
);
// 注意这里的调用,
// 要结合上面对 PropertyCont 类型的解释来理解
properties.forEach(cont => cont(prop => {
// TS 可以推断出 val 的类型是 `T`
const val = prop.pget();
prop.pset(val);
}));
有了上面的理解,我们再看 StackOverflow 上的一个问题。
有一个泛型接口如下:
interface Transform<ArgType> {
transformer: (input: string, arg: ArgType) => string;
arg: ArgType;
}
如果要给一个字符串应用一组 Transform
,这组 Transform
的类型要如何定义?代码大概像下面这样:
function append(input: string, arg: string): string {
return input.concat(arg);
}
function repeat(input: string, arg: number): string {
return input.repeat(arg);
}
const transforms = [
{
transformer: append,
arg: " END"
},
{
transformer: repeat,
arg: 4
},
];
function applyTransforms(input: string, transforms *什么类型?*): string {
for (const transform of transforms) {
input = transform.transformer(input, transform.arg);
}
return input;
}
有了前面对 Existential Types 的解释,我们这样解决:
type TransCont = <R>(cont: <T>(tran: Transform<T>) => R) => R;
function makeTransCont<T>(tran: Transform<T>): TransCont {
return (cont) => cont(tran);
}
function applyTransforms(input: string, transforms: TransCont[]): string {
for (const tran of transforms) {
tran(tr => {
input = tr.transformer(input, tr.arg);
});
}
return input;
}
// 或者这样实现:
function applyTransformsV2(input: string, transforms: TransCont[]): string {
return transforms.reduce((p, v) => v(tr => tr.transformer(p, tr.arg)), input);
}
使用起来也并不困难:
const tr: TransCont[] = [];
tr.push(makeTransCont({
transformer: append,
arg: " END"
}));
tr.push(makeTransCont({
transformer: repeat,
arg: 4
}));
const t = applyTransforms('Hello', tr);
console.log(t);
1总结
对于实现了同一泛型接口的对象,如果要将其放在数组内使用,又要保持各自的泛型类型,Existential Types 提供了一个可行的方案。
参考资料
Existential Types in Typescript Through Continuations: https://www.jalo.website/existential-types-in-typescript-through-continuations
[2]
How do you define an array of generics in TypeScript?: https://stackoverflow.com/questions/51879601/how-do-you-define-an-array-of-generics-in-typescript
原文始发于微信公众号(背井):TypeScript 中的 Existential Types
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246650.html