抽象掉打字稿中的泛型类型参数

hjf*_*yer 5 typescript

我遇到了any在 Typescript 中似乎无法避免的情况。这是一个反映我正在尝试做的事情的示例:

type NativeFn<A, B> = {
    kind: 'native'
    fn: (a: A) => B
}

type ComposeFn<A, B, C> = {
    kind: 'compose'
    f: Fn<B, C>
    g: Fn<A, B>
}

type Fn<A, B> = NativeFn<A, B>
    | ComposeFn<A, any, B>  // <=== HERE

function evalfn<A, B>(fn: Fn<A, B>, arg: A): B {
    switch (fn.kind) {
        case 'native': return fn.fn(arg)
        case 'compose': {
            // intermediate has type "any", which is a drag.
            const intermediate = evalfn(fn.g, arg)
            return evalfn(fn.f, intermediate)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我想说的是,无论是哪种类型,ComposeFn<A, B, C>始终是 a ,但仍应键入。Fn<A, C>BB

使用any,我可以错误地输入以下内容:

const F: Fn<string, string[]> = { kind: 'native', fn: (n) => [n] }

const G: Fn<number, number> = { kind: 'native', fn: (n) => n + 1 }

const FoG: Fn<number, string[]> = {
    kind: 'compose',
    f: F,
    g: G,
}
Run Code Online (Sandbox Code Playgroud)

unknown也不起作用。例子

有什么办法可以完成我在这里要做的事情吗?

jca*_*alz 1

TypeScript 不直接支持存在类型(请参阅microsoft/TypeScript#14466)。如果是这样,您将使用存在类型<\xe2\x88\x83T> ComposeFn<string, T, number>来表示“有某种类型T,我的类型是ComposeFn<string, T, number>,但我不知道或不关心它是什么”:

\n\n
// not valid TS syntax, do not try this\ntype Fn<A, B> = NativeFn<A, B> | <\xe2\x88\x83T> ComposeFn<A, T, B> \n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

如果没有对此类类型的直接支持,则有多种解决方法。到目前为止,最简单的一种是代替any存在类型,不用太担心。是的,这会导致不安全的事情发生,但 TypeScript 并不是真正的完美类型安全语言(请参阅microsoft/TypeScript#9825),也不是有意为之(请参阅TypeScript Design Non-Goal #3),而且any确实非常非常方便的。

\n\n

Fn通过允许指定“存在”参数并将其默认为,您可能会获得一点额外的安全性any,如下所示:

\n\n
type Fn<A, B, T = any> = NativeFn<A, B>\n    | ComposeFn<A, T, B>\n\nfunction evalfn<A, B, T>(fn: Fn<A, B, T>, arg: A): B {\n    switch (fn.kind) {\n        case \'native\': return fn.fn(arg)\n        case \'compose\': {\n            const intermediate = evalfn(fn.g, arg)\n            return evalfn(fn.f, intermediate)\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

它并不完美,但至少evalfn如果你做了一些太奇怪的事情,编译器会抱怨。

\n\n
\n\n

如果你真的想要类型安全,你可以通过使用泛型函数实现来模拟存在类型;如果我有一个像这样的函数签名<T>(x: T, y: T) => void,那么调用者可以选择T,并且实现者需要将其视为T存在类型参数。通过切换谁是调用者和谁是实现者,它为我们提供了一种奇怪的由内而外的方式来Fn<A, B>以类型安全的方式表示:

\n\n
type SomeComposeFn<A, B> = {\n  kind: \'compose\',\n  <R>(go: <T>(f: Fn<T, B>, g: Fn<A, T>) => R): R\n}\n\ntype Fn<A, B> = NativeFn<A, B> | SomeComposeFn<A, B>\n
Run Code Online (Sandbox Code Playgroud)\n\n

在这里, aSomeComposeFn<A, B>本身就是一个函数,其作用类似于和函数Promise的a 。然后你可以像这样实现:fgevalfn

\n\n
function evalfn<A, B>(fn: Fn<A, B>, arg: A): B {\n  switch (fn.kind) {\n    case \'native\': return fn.fn(arg)\n    case \'compose\': {\n      return fn((f, g) => {\n        const intermediate = evalfn(g, arg)\n        return evalfn(f, intermediate)\n      });\n    }\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果你有一个原始ComposeFn<A, T, B>类型的值,你可以SomeComposeFn<A, B>以一种简单的方式将其转换为 a ,就像解析一个 Promise 一样:

\n\n
function someComposeFn<A, T, B>(composeFn: ComposeFn<A, T, B>): SomeComposeFn<A, B> {\n  return Object.assign(\n    <R>(go: <T>(f: Fn<T, B>, g: Fn<A, T>) => R) => go(composeFn.f, composeFn.g),\n    { kind: "compose" as const });\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此,这按照您想要的方式是完全类型安全的,但很麻烦,不是我可能会选择的方法。当然,这取决于你。

\n\n
\n\n

好的,希望能给您一些指导;祝你好运!

\n\n

Playground 代码链接

\n