条件类型与交叉类型

huangapple go评论61阅读模式
英文:

Conditional type with intersection

问题

我想在TypeScript中创建一个函数,其输出类型取决于输入类型。但我还希望类型包括输入类型的交集。看起来我可以使用交集类型来实现这一点。然而,这不会为工厂的交集生成重载:

例如:

interface InputA {
  hello: string;
}

interface OutputA {
  world: string;
}

interface FactoryA {
  create(input: InputA): OutputA;
}

interface InputB {
  foo: string;
}

interface OutputB {
  bar: string;
}

interface FactoryB {
  create(input: InputB): OutputB;
}

interface InputC {
  foo: string;
  baz: string;
}

interface OutputC {
  blah: string;
}

interface FactoryC {
  create(input: InputC): OutputC;
}

// 示例所需的输入/输出

const combinedFactory: CombinedFactory<[FactoryA, FactoryB, FactoryC]> = ...;

// OutputA
combinedFactory.create({ hello: '' });

// OutputB
combinedFactory.create({ foo: '' });

// OutputA & OutputB
combinedFactory.create({ hello: '', foo: '' });

// OutputB & OutputC
combinedFactory.create({ foo: '', baz: '' });

// OutputA & OutputB & OutputC
combinedFactory.create({ hello: '', foo: '', baz: '' });

// 错误:不满足InputA | InputB | InputC
combinedFactory.create({ nope: '' });

我可以手动编写重载,但如果我引入更多的工厂类型,这可能会变得繁琐。是否有一种方便的方式在TypeScript中实现这一点?

英文:

I'd like to create a function in Typescript whose output type depends on the input type. But I'd also like the typings to include intersections of the input types. It looks like I can almost do this with an intersection type. However, this won't generate an overload for the intersection of the factories:

For example:

interface InputA {
  hello: string;
}

interface OutputA {
  world: string;
}

interface FactoryA {
  create(input: InputA): OutputA;
}

interface InputB {
  foo: string;
}

interface OutputB {
  bar: string;
}

interface FactoryB {
  create(input: InputB): OutputB;
}

interface InputC {
  foo: string;
  baz: string;
}

interface OutputC {
  blah: string;
}

interface FactoryC {
  create(input: InputC): OutputC;
}

// Example desired inputs/outputs

const combinedFactory: CombinedFactory&lt;[FactoryA, FactoryB, FactoryC]&gt; = ...;

// OutputA
combinedFactory.create({ hello: &#39;&#39;});

// OutputB
combinedFactory.create({ foo: &#39;&#39;});

// OutputA &amp; OutputB
combinedFactory.create({ hello: &#39;&#39;, foo: &#39;&#39;});

// OutputB &amp; OutputC
combinedFactory.create({ foo: &#39;&#39;, baz: &#39;&#39;});

// OutputA &amp; OutputB &amp; OutputC
combinedFactory.create({ hello: &#39;&#39;, foo: &#39;&#39;, baz: &#39;&#39;});

// Error: Does not satisfy InputA | InputB | InputC
combinedFactory.create({ nope: &#39;&#39; });

I could write the overloads manually, but this can get cumbersome if I then introduce more factory types.

Is there a convenient way to implement this in typescript?

答案1

得分: 1

以下是您要的代码翻译:

这是一种可能的方法,尽管它相当复杂:

    interface Factory<I, O> {
        create(input: I): O;
    }
    
    type CombinedFactory<F extends Factory<any, any>[]> = {
        create<I extends { [N in keyof F]:
            F[N] extends Factory<infer IN, any> ?
            IN : never
        }[number]>(
            input: I
        ): I extends any ? { [N in keyof F]:
            F[N] extends Factory<infer IN, infer ON> ? I extends IN ?
            (x: ON) => void : never : never
        }[number] extends (x: infer O) => void ? O : never : never;
    };

首先,`CombinedFactory<F>` 是关于 `F` 的泛型,它被限制为 `Factory` 类型的元组。它具有一个泛型的 `create()` 方法,该方法的泛型参数是 `I`,表示 `input` 参数的允许类型。

对于 `I` 的约束涉及到 `{ [N in keyof F]: F[N] extends Factory<infer IN, any> ? IN : never }`,这是一个映射的元组类型,它遍历 `F` 元组的每个类似数字的索引 `N`,并将该索引处的元素映射为工厂输入类型 `IN`。因此,它是一个输入类型的元组。然后,我们通过 `number` 对其进行索引,以获得这些工厂输入类型的联合。因此,`I` 必须可分配给 `F` 元组中允许的工厂输入的联合。

输出类型更为复杂。首先,它被包裹在看似无用的 `I extends any ? ... : never` 中,实际上这是一个分布式条件类型,会导致 `I` 中的联合导致输出中的联合。因此,如果 `create(i1)` 输出类型是 `O1`,而 `create(i2)` 输出类型是 `(O2)`,那么 `create(Math.random() < 0.5 ? i1 : i2)` 的输出类型将是 `O1 | O2`

无论如何,在该类型内部,我们有另一个映射的元组。这一次,对于每个类似数字的索引 `N`,它推断出相应的 `F[N]` 元组中的输入类型 `IN` 和输出类型 `ON`。然后,它检查 `I`(现在不是联合,因为外层的分布条件类型)是否可分配给工厂输入类型 `IN`。如果是的话,我们想保留输出类型 `ON`,否则将其丢弃为 `never`。实际上,我们将 `ON` 映射为函数参数 `(x: ON) => void`,因此我们最终得到一个包含所有适用工厂输出类型的函数参数的元组,不适用的类型为 `never`。因此,如果 `F`  `[Factory<I1, O1>, Factory<I2, O2>, Factory<I3, O3>]`,而 `I`  `I1 & I3`,那么映射类型将是 `[(x: O1) => void, never, (x: O3) => void]`。我们之所以将这些放入函数中,是因为这些是*逆变*类型位置(如 https://stackoverflow.com/q/66410115/2887218 中所述),稍后将用于给我们提供交集。

现在,我们取该映射的元组并使用 `number` 对其进行索引,以获得这些函数的联合(因此在此示例中是 `((x: O1) => void) | ((x: O3) => void)`),并推断其与单个函数参数输入进行交集。由于[条件类型推断](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types)的工作方式,这给我们了交集 `O1 & O3`。这与 https://stackoverflow.com/q/50374908/2887218 中描述的基本技术相同。

因此,`I` 中的联合应返回联合输出,并且`I` 中的联合成员中的交集应返回输出中的交集。

---

让我们用您的示例来测试一下:

```typescript
declare const combinedFactory: CombinedFactory<[FactoryA, FactoryB, FactoryC]>;

const oA = combinedFactory.create({ hello: '' });
// const oA: OutputA

const oB = combinedFactory.create({ foo: '' });
// const oB: OutputB

const oAandB = combinedFactory.create({ hello: '', foo: '' });
// const oAandB: OutputA & OutputB

const oAorB = combinedFactory.create(Math.random() < 0.5 ? { hello: "" } : { foo: "" });
// const oAorB: OutputA | OutputB

const oBandC = combinedFactory.create({ foo: '', baz: '' });
// const oBandC: OutputB & OutputC

const oAandBandC = combinedFactory.create({ hello: '', foo: '', baz: '' });
// const oABC: OutputA & OutputB & OutputC

combinedFactory.create({ nope: '' }); // error!
// Argument of type '{ nope: string; }' is not assignable to
// parameter of type 'InputA | InputB | InputC'.

看起来很不错。InputAInputB 的交集分别产生 OutputAOutputB 的交集。InputC 的输入会产生 OutputB & OutputC,因为InputC 可分配给InputBInputA & InputB & InputC 的输入会产生 OutputA & OutputB & OutputC 的输出。不合法的输入会被拒绝,因为它无法分配给允许的输入类型的联合。

英文:

Here's one possible approach, although it's fairly involved:

interface Factory&lt;I, O&gt; {
create(input: I): O;
}
type CombinedFactory&lt;F extends Factory&lt;any, any&gt;[]&gt; = {
create&lt;I extends { [N in keyof F]:
F[N] extends Factory&lt;infer IN, any&gt; ?
IN : never
}[number]&gt;(
input: I
): I extends any ? { [N in keyof F]:
F[N] extends Factory&lt;infer IN, infer ON&gt; ? I extends IN ?
(x: ON) =&gt; void : never : never
}[number] extends (x: infer O) =&gt; void ? O : never : never;
};

First, CombinedFactory&lt;F&gt; is generic in F, which is constrained to be a tuple of Factory types. It has a generic create() method which is generic in I, the allowable type of the input parameter.

The constraint on I involves { [N in keyof F]: F[N] extends Factor&lt;infer IN, any&gt; ? IN : never }, which is a mapped tuple type which iterates over each numeric-like index N from the F tuple, and maps the element at that index to just the factory input type there IN. So it's a tuple of input types. Then we index into it number to get the union of those factory input types. Thus I must be assignable to the union of allowable factory inputs from the F tuple.

The output type is even more involved. First it is wrapped in a seemingly useless I extends any ? ... : never, which is in actuality a distributive conditional type which causes unions in I to lead to unions in the output. So if create(i1) outputs type O1, and create(i2) output types (O2), then create(Math.random()&lt;0.5?i1:i2) will output type O1 | O2.

Anyway, inside that type we have another mapped tuple. This time, for each numeric-like index N, it infers the input type IN and output type ON for the corresponding F[N] element of the F tuple. It then checks to see if I (which can't be a union now because of the outermost distributive conditional type) is assignable to the factory input type IN. If so, then we want to hold onto ON for the output, otherwise we discard it to never. What we actually do is map ON to a function parameter (x: ON) =&gt; void, so we end up with a tuple with all the applicable factory output types as function parameters, and the inapplicable ones as never. So if F is [Factory&lt;I1,O1&gt;, Factory&lt;I2,O2&gt;, Factory&lt;I3,O3&gt;], and I is I1 &amp; I3, then the mapped type is [(x: O1)=&gt;void, never, (x: O3)=&gt;void]. The reason why we put these in functions is because these are contravariant type positions (as mentioned in https://stackoverflow.com/q/66410115/2887218 ) and this will be used later to give us intersections.

So now we take that mapped tuple and index into it with number to give us the unions of those functions (so ((x: O1) =&gt; void) | ((x: O3) =&gt; void) in the example here), and infer that against a single function parameter input. Because of the way conditional type inference works, this gives us the intersection O1 &amp; O3. (This is the same basic technique described in https://stackoverflow.com/q/50374908/2887218 .)

So a union in I should return a union output, and intersections in union members of I should return intersections in union members of the output.


Let's test it out with your examples:

declare const combinedFactory:
CombinedFactory&lt;[FactoryA, FactoryB, FactoryC]&gt;;
const oA = combinedFactory.create({ hello: &#39;&#39; });
// const oA: OutputA
const oB = combinedFactory.create({ foo: &#39;&#39; });
// const oB: OutputB
const oAandB = combinedFactory.create({ hello: &#39;&#39;, foo: &#39;&#39; });
// const oAandB: OutputA &amp; OutputB
const oAorB = combinedFactory.create(Math.random() &lt; 0.5 ? { hello: &quot;&quot; } : { foo: &quot;&quot; });
// const oAorB: OutputA | OutputB
const oBandC = combinedFactory.create({ foo: &#39;&#39;, baz: &#39;&#39; });
// const oBandC: OutputB &amp; OutputC
const oAandBandC = combinedFactory.create({ hello: &#39;&#39;, foo: &#39;&#39;, baz: &#39;&#39; });
// const oABC: OutputA &amp; OutputB &amp; OutputC
combinedFactory.create({ nope: &#39;&#39; }); // error!
// Argument of type &#39;{ nope: string; }&#39; is not assignable to
// parameter of type &#39;InputA | InputB | InputC&#39;.

Looks good. The intersection and union of InputA and InputB produces, respectively, the intersection and union of OutputA and OutputB. The input of type InputC produces OutputB &amp; OutputC because InputC is assignable to InputB. And the input of type InputA &amp; InputB &amp; InputC produces the output of OutputA &amp; OutputB &amp; OutputC. And the bad input is rejected because it's not assignable to the union of allowable input types.

Playground link to code

答案2

得分: 0

以下是代码部分的翻译:

你可以使用泛型来实现这一点:
请注意 `Factory&lt;InputA &amp; InputB, OutputA &amp; OutputB&gt;`,它添加了所需的重载。
```lang-ts
interface Factory&lt;InputType, OutputType&gt; {
create(input: InputType): OutputType;
}
type CombinedFactory = Factory&lt;InputA, OutputA&gt; &amp; Factory&lt;InputB, OutputB&gt; &amp; Factory&lt;InputA &amp; InputB, OutputA &amp; OutputB&gt;;
const combinedFactory: CombinedFactory = ...;
// 返回 OutputA
const result1 = combinedFactory.create({ hello: &#39;hello&#39;});
// 返回 OutputB
const result2 = combinedFactory.create({ foo: &#39;hello&#39;});
// 返回 OutputA &amp; OutputB
const result3 = combinedFactory.create({ hello: &#39;hello&#39;, foo: &#39;hello&#39;});

在 TS Playground 中查看代码


请注意,代码部分未被直接翻译,因为它包含特定的编程代码和标记,而这些标记在中文中没有直接对应。如果您需要进一步的解释或翻译,请随时提出。
<details>
<summary>英文:</summary>
You can accomplish that with generics:
Notice `Factory&lt;InputA &amp; InputB, OutputA &amp; OutputB&gt;` which adds desired overload.
```lang-ts
interface Factory&lt;InputType, OutputType&gt; {
create(input: InputType): OutputType;
}
type CombinedFactory = Factory&lt;InputA, OutputA&gt; &amp; Factory&lt;InputB, OutputB&gt; &amp; Factory&lt;InputA &amp; InputB, OutputA &amp; OutputB&gt;;
const combinedFactory: CombinedFactory = ...;
// Returns OutputA
const result1 = combinedFactory.create({ hello: &#39;hello&#39;});
// Returns OutputB
const result2 = combinedFactory.create({ foo: &#39;hello&#39;});
// Returns OutputA &amp; OutputB
const result3 = combinedFactory.create({ hello: &#39;hello&#39;, foo: &#39;hello&#39;});

Code in TS Playground

huangapple
  • 本文由 发表于 2023年2月10日 02:47:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75403128.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定