【问题标题】:TypeScript Type Inference with Intersection of Generic TypesTypeScript 类型推断与泛型类型的交集
【发布时间】:2021-10-18 20:28:56
【问题描述】:

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

这是代码的简化版本:

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&lt;T extends A&lt;any&gt; &amp; B&lt;any&gt;&gt;,但这当然允许任何泛型类型的组合。

然后我尝试添加一个额外的类型S 以确保两个泛型相同:test&lt;S, T extends A&lt;S&gt; &amp; B&lt;S&gt;&gt;。但这会失败,因为 “参数类型 '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'

但正如您所见,它很难阅读,也很难写,尤其是当交集包含更多类型时。

有没有更简单的方法来完成这项工作?

【问题讨论】:

  • 为什么还需要T?你应该只使用S(重命名为There)。即使是示例用例也不能保证返回的值是正确的类型。如果交集包含很多类型,则给它一个别名,如type I&lt;T&gt;= A&lt;T&gt; &amp; B&lt;T&gt; &amp; C&lt;T&gt; &amp; ...,然后只是function test&lt;T&gt;(obj: I&lt;T&gt;)。如果这些建议与您的用例不匹配,您可能需要详细说明并提供说明原因的minimal reproducible example
  • 但是现在你返回A&lt;T&gt; &amp; B&lt;T&gt;,这不是我想要的。我想返回与函数参数相同的类型。例子:如果你有type X = A &amp; B &amp; C,那么test应该返回A &amp; B &amp; C,即使test只需要A &amp; B就可以工作。我将在问题中添加一个示例
  • 好的,那么this 更适合你吗?
  • 如果是这样我可以写一个答案;如果没有,请告诉我缺少什么。
  • 我猜,你是个魔术师。我所有的测试都通过了。让我试着解释一下你的解决方案:"function test 接受一个可以是任何东西的参数(通用类型 O 没有约束)并且它必须实现 I&lt;T&gt;(等于到A&lt;T&gt; &amp; B&lt;T&gt;),并且函数返回该类型"。我不明白的是:为什么test&lt;S, T extends A&lt;S&gt; &amp; B&lt;S&gt;&gt;(obj: T): T 不起作用?也许您可以写一个简短的答案并获得当之无愧的积分。

标签: typescript generics type-inference typescript-generics inferred-type


【解决方案1】:

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

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

如果您希望编译器从对函数 func(param) 的调用中推断出类型参数 X,其中 param 的类型为 P,并且 func 的调用签名类似于 &lt;X&gt;(param: F&lt;X&gt;) =&gt; any,则编译器会将P 插入F&lt;X&gt; 以获取X 的候选对象。

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

因此,对于调用签名&lt;O, T&gt;(obj: O &amp; A&lt;T&gt; &amp; B&lt;T&gt;) =&gt; Otest(obj),编译器将倾向于将Otypeof obj 匹配以获取O 的候选对象,然后还将A&lt;T&gt;B&lt;T&gt; 匹配到@ 987654348@ 获取T 的候选人。如果obj 不是有效的A&lt;T&gt; &amp; B&lt;T&gt;,则推理将失败。您可以使用O 来表示obj 的实际类型,而无需将其扩大到A&lt;T&gt; &amp; B&lt;T&gt;

所以这就是你想要的方式:

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 extends ... 这样的条件类型中的参数X,因此typeof objO 的候选对象,它然后检查A&lt;infer T&gt; &amp; B&lt;infer T&gt;。如果找到有效的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&lt;T&gt; &amp; B&lt;T&gt; 推断出T,但一般约束不会以这种方式用作推断站点。请参阅microsoft/TypeScript#7234 以获取支持这一点的旧建议。他们没有实现这样的功能,而是suggested 人们完全按照我在此答案开头所做的方式从交叉路口推断。

无论如何,如果没有T 的推理站点,编译器就会退回到unknown。然后O 被限制为A&lt;unknown&gt; &amp; B&lt;unknown&gt;,这不太可能奏效。所以一切都崩溃了。

Playground link to code

【讨论】:

    猜你喜欢
    • 2019-09-17
    • 1970-01-01
    • 1970-01-01
    • 2017-10-04
    • 2018-01-18
    • 2016-12-05
    • 1970-01-01
    • 2021-07-28
    • 1970-01-01
    相关资源
    最近更新 更多