TypeScript 可变元组类型推断问题,用于运行一系列 "Result" 返回函数的函数

TypeScript variadic tuple type inference issue, for function that runs a sequence of "Result"-returning functions

(TypeScript playground 中的完整代码:link。)

我有一个 Result<T, E> 类型的计算,它要么以 T 类型的值成功,要么以 E.

类型的错误失败
type Result<T, E> =
    | {ok: true, value: T}
    | {ok: false, value: E};

我想编写一个辅助函数,它可以 运行 按顺序排列 Result-return 函数列表。如果任何函数失败,停止处理并 return 错误。如果全部成功,return 成功值列表。

以下是专门针对长度为三的列表实现该功能的方法:

function sequenceFixed<T1, T2, T3, E>(fns: [() => Result<T1, E>, () => Result<T2, E>, () => Result<T3, E>]): Result<[T1, T2, T3], E> {
    const [f1, f2, f3] = fns;
    const r1 = f1();
    if (!r1.ok) return r1;
    const r2 = f2();
    if (!r2.ok) return r2;
    const r3 = f3();
    if (!r3.ok) return r3;
    return {ok: true, value: [r1.value, r2.value, r3.value]};
}

我想使用 TypeScript 4.0 的可变元组类型使它适用于任何长度的列表。 (函数体本身不需要类型检查,只需要检查调用点。)

这是我的两次尝试:

declare function sequenceGeneric1<T extends Array<unknown>, E>(
    fns: Readonly<{[P in keyof T]: () => Result<T[P], E>}>,
): Result<[...T], E>;

declare function sequenceGeneric2<Fns extends Array<unknown>, E>(
    fns: Fns,
): Result<{[P in keyof Fns]: ExtractResultSuccessType<Fns[P]>}, E>;

type ExtractResultSuccessType<Fn> = Fn extends () => Result<infer T, unknown> ? T : unknown;

如果我显式传递类型参数,调用这些函数会正确进行类型检查,但我不知道如何让 TypeScript 推断类型参数。

declare function f1(): Result<string, string>;
declare function f2(): Result<boolean, string>;
declare function f3(): Result<number, string>;

function checkFixed() {
    return sequenceFixed([f1, f2, f3]);
}
function checkGeneric1() {
    return sequenceGeneric1([f1, f2, f3]);
}

function checkGeneric2() {
    return sequenceGeneric2([f1, f2, f3]);
}

function check() {
    // ok
    const c1: Result<[string, boolean, number], string> = checkFixed();

    // ERROR: Type 'Result<(string | number | boolean)[], unknown>' is not assignable to
    //        type 'Result<[string, boolean, number], string>'.
    const c2: Result<[string, boolean, number], string> = checkGeneric1();
    
    // ERROR: Type 'Result<(string | number | boolean)[], unknown>' is not assignable to
    //        type 'Result<[string, boolean, number], string>'.
    const c3: Result<[string, boolean, number], string> = checkGeneric2();
}

function checkGeneric1WithAnnontation(): Result<[string, boolean, number], string> {
    return sequenceGeneric1<[string, boolean, number], string>([f1, f2, f3]);
}

在这种情况下,走你的 sequenceGeneric2 路线肯定更容易,你有一个直接对应于传入参数类型的通用参数;毕竟,编译器从类型 T 的值推断类型 T 比编译器从类型 [=] 的值推断类型 T 更直接17=].

在您的情况下,您使用 Fns 朝着正确的方向前进,但您仍然期望编译器推断 E,但这样做并不合适,因为您无法在 sequenceGeneric1.

中很好地推断 T 元组的原因相同

还有一件事:如果将数组文字传递给函数,编译器很有可能会为其推断无序数组类型而不是元组类型。有一些方法可以向编译器提示您更喜欢推断元组类型。一种方法是使用可变元组表示法...因此您可以写 fns: readonly [...Fns] 而不是写 fns: Fns。这在 fns 可以是什么类型方面没有太大区别,但它确实导致编译器更喜欢元组类型 Fns.


所以,继续:让我们只有一个泛型类型参数,Fns,然后让编译器计算 T 类型 E 输入输出:

declare function sequenceGeneric<Fns extends Array<() => Result<any, any>>>(
    fns: readonly [...Fns],
): Result<
  { [P in keyof Fns]: ExtractResultSuccessType<Fns[P]> },
  ExtractResultErrorType<Fns[keyof Fns]>
>;

哪里

type ExtractResultSuccessType<Fn> = Fn extends () => Result<infer T, any> ? T : never;
type ExtractResultErrorType<Fn> = Fn extends () => Result<any, infer E> ? E : never;

我已将推断类型中的 unknown 更改为 any,因为它往往更易于编译器验证。 unknown 是真正的顶级类型,有时会以您并不真正想要的方式与 covariance/contravariance 严格配合。


让我们看看它是否有效:

function checkGeneric() {
    return sequenceGeneric([f1, f2, f3]);
}
// function checkGeneric(): Result<[string, boolean, number], string>

看起来不错!


Playground link to code