【问题标题】:Generic pattern-matching for tagged unions标记联合的通用模式匹配
【发布时间】:2020-01-23 05:17:50
【问题描述】:

我正在编写一个通用模式匹配函数match 用于标记联合,将对象作为匹配器传递(用于代替典型的switch (obj.kind) { ... })。这就是我所拥有的:

type UnionNamespace<Obj extends { kind: string }> = {
    [K in Obj["kind"]]: Obj extends { kind: K } ? Obj : never;
};

type Matcher<Obj extends { kind: Kind }, Result, Kind extends string> = {
    [K in Kind]: (obj: UnionNamespace<Obj>[K]) => Result;
};

function match<Obj extends { kind: Kind }, Result, Kind extends string>(
    obj: Obj,
    matcher: Matcher<Obj, Result, Kind>
): Result {
    const fn = matcher[obj.kind];
    return fn(obj as Parameters<typeof fn>[0]);
}

/* Example */

type Square = { kind: "square"; side: number };
type Circle = { kind: "circle"; radius: number };
type Shape = Square | Circle;

const square = { kind: "square", side: 2 } as Shape;
const surface = match(square, {
    square: square => square.side ** 2,
    circle: circle => Math.PI * circle.radius ** 2
});

console.log(surface.toFixed()); // Op that does not type-check if surface is not a number

我对代码并不完全满意。例如,1) 我不想提示Kind extends string,但后来我得到Result=unknown。另外,2) 这个Parameters&lt;typeof fn&gt;[0] 看起来有点笨拙,但这是我发现的唯一一种对调用进行类型检查的方法。

有什么想法/建议吗?你知道任何现有的代码可以做这样的事情吗?

[编辑] 最终版本,可选择区分字段:

https://gist.github.com/tokland/c0db1473cc9bfa924470e52bdac8450c

【问题讨论】:

    标签: typescript pattern-matching


    【解决方案1】:

    我看不出你的代码有什么问题;甚至是你有一个“不必要的”泛型类型参数的部分,因为它可以帮助编译器推断你想要什么。

    您可以采取或离开的可能修改方法是:

    type Matcher<Obj extends { kind: string }> = {
        [K in Obj["kind"]]: (obj: Extract<Obj, { kind: K }>) => any;
    };
    
    function match<T extends { kind: string }, M extends Matcher<T>>(
        obj: T,
        matcher: M
    ): ReturnType<M[T["kind"]]> {
        const fn = matcher[obj.kind as T["kind"]];
        return fn(obj as Parameters<typeof fn>[0]);
    }
    

    这里的想法是,编译器从X 类型的值推断类型参数X 比从SomeTypeFunction&lt;X&gt; 类型的值推断类型参数X 要好得多。所以,我做了两个类型参数:T对应objM对应matcher。然后使用类型函数计算match(obj, matcher) 的返回类型,作为TM... 的函数...在本例中为ReturnType&lt;M[T["kind"]]&gt;

    match() 的实现中,编译器似乎无法在没有被提醒的情况下理解obj.kindT["kind"] 类型,但除此之外它是相同的。它在示例代码中的行为与您的类似:

    type Square = { kind: "square"; side: number };
    type Circle = { kind: "circle"; radius: number };
    type Shape = Square | Circle;
    
    const square = { kind: "square", side: 2 } as Shape;
    const surface = match(square, {
        square: square => square.side ** 2,
        circle: circle => Math.PI * circle.radius ** 2
    });
    
    console.log(surface.toFixed()); // toFixed() doesn't work if not a number
    

    看起来不错。当然,这个世界充满了边缘情况,因此这两种方法之间无疑存在差异,这可能使一种方法比另一种更适合您的用例。无论如何,希望有所帮助;祝你好运!

    Playground link to code

    【讨论】:

    • 谢谢,@jcalz;与往常一样,您的答案中有很多知识。我将对其进行探索并了解每种方法的优缺点。
    猜你喜欢
    • 2015-04-08
    • 1970-01-01
    • 1970-01-01
    • 2014-05-07
    • 1970-01-01
    • 2017-08-18
    • 1970-01-01
    • 1970-01-01
    • 2019-01-10
    相关资源
    最近更新 更多