TypeScript 类型推断与通用类型的交集

TypeScript Type Inference with Intersection of Generic Types

我有一个用例,其中我有一个可组合对象,它应该(不)与某些功能一起使用,具体取决于它的组成部分。组合是使用类型交集完成的。

这是代码的简化版本:

type A<T> = { a: T }
type B<T> = { b: (val: T) => T }

const shouldWork = {
  a: 'str',
  b: (val: string) => val.toUpperCase(),
  someOtherProp: 'foo'
}

const shouldFail = {
  a: 'str',
  b: (val: number) => 42
}

function test<T extends A<???> & B<???>>(obj: T): T {
  return {
    ...obj,
    a: obj.b(obj.a)
  }
}

const res1 = test(shouldWork);
// res1 should be typeof shouldWork, i.e. A & B & { someOtherProp: string }
console.log(res1.a); // "STR"
console.log(res1.someOtherProp); // "foo"

const res2 = test(shouldFail); // should fail because A<string> and B<number> don't match

我试过使用 test<T extends A<any> & B<any>>,但这当然会允许泛型类型的任意组合。

然后我尝试添加一个额外的类型 S 以确保两个泛型相同:test<S, T extends A<S> & B<S>>。但这会失败,因为 "参数类型 'val' 和 'val' 不兼容。类型 'unknown' 不可分配给类型 'string'"

经过更多尝试后,我发现了一些可以按预期工作的东西:

function test<S, T extends A<S> & B<S> = A<S> & B<S>>(obj: T): T { ... }
const res1 = test<string>(shouldWork);
const res2 = test<string>(shouldFail); // throws error: Types of parameters 'val' and 'val' are incompatible. Type 'string' is not assignable to type 'number'

但是如你所见,读起来很困难,写起来也很困难,尤其是当交集包含更多类型时。

是否有更简单的方法来完成此操作?

这有点“不确定”,但是实现您想要的推理的一种方法是这样的:

function test<O, T>(obj: O & A<T> & B<T>): O {
    return {
        ...obj,
        a: obj.b(obj.a)
    }
}

当您希望编译器从对函数 func(param) 的调用中推断类型参数 X 时,其中 param 的类型为 P 且调用签名为func 类似于 <X>(param: F<X>) => any,编译器将为 F<X> 插入 P 以获得 X.

的候选项

如果调用签名涉及 intersection type,如 <X>(param: F<X> & G<X>) => void,编译器将倾向于为 both [=21] 插入 P =] G<X> 来获得候选人......即使只为其中一个人这样做是好的(有时是可取的)。

因此,对于 test(obj) 的调用签名 <O, T>(obj: O & A<T> & B<T>) => O,编译器将倾向于将 Otypeof obj 匹配以获得 O 的候选对象,并且然后还匹配 A<T>B<T>typeof obj 以获得 T 的候选。如果 obj 不是有效的 A<T> & B<T>,推理将失败。您可以使用 O 来表示 obj 的实际类型,而无需将其扩展为 A<T> & B<T>.

所以这会按照您想要的方式运行:

const res1 = test(shouldWork); // okay
const res2 = test(shouldFail); // error
// -------------> ~~~~~~~~~~
// Argument of type '{ a: string; b: (val: number) => number; }' is not assignable to
//  parameter of type '{ a: string; b: (val: number) => number; } & A<number> & B<number>'.

如果这不起作用,您可以通过使用单个不受约束的类型参数 O 并使参数 obj 成为 conditional type 来获得类似的行为:

function test<O>(obj: O extends (A<infer T> & B<infer T>) ? O : never): O {
    return {
        ...obj,
        a: obj.b(obj.a)
    }
}

const res1 = test(shouldWork); // okay
const res2 = test(shouldFail); // error\
// -------------> ~~~~~~~~~~
// Argument of type '{ a: string; b: (val: number) => number; }' \
// is not assignable to parameter of type 'never'

这是可行的,因为编译器倾向于将参数 P 的类型插入到条件类型 X 中,例如 X extends ...,因此 typeof objO 的候选者,然后根据 A<infer T> & B<infer T> 进行检查。如果找到有效的 T,则 obj 将再次与 O 进行检查,这没有问题。如果未找到有效的 T,则将根据 never 检查 obj,这可能不会起作用。它有点复杂,但具有相似的行为(错误消息不太容易理解)。


至于以下为什么不起作用:

declare function test<O extends A<T> & B<T>, T>(obj: O): O;

问题是T没有推理站点。编译器可以使用 typeof obj 来推断 O,但是当它检查 T 时,它被卡住了。您可能希望编译器可以从 Oconstraint A<T> & B<T>, but generic constraints are not used as inference sites this way. See microsoft/TypeScript#7234 for an old suggestion to support this. Instead of implementing such a feature, they suggested 中推断出 T 人们从交叉点推断出的方式与我在本答案开头所做的完全相同。

无论如何,T 没有推理站点,编译器会回退到 unknown。然后 O 被限制为 A<unknown> & B<unknown>,这不太可能起作用。所以一切都崩溃了。

Playground link to code