TypeScript 中的 Existential Types

这里要明确两点:

  1. TypeScript 并不(直接)支持 Existential Types。

  2. 什么是 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<???>[];

你当然可以使用 anyunknown。但使用 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 提供了一个可行的方案。

参考资料

[1]

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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!