【问题标题】:A generic TypeScript interface with method that returns exactly one of union type一个泛型 TypeScript 接口,其方法只返回一种联合类型
【发布时间】:2021-08-10 19:05:43
【问题描述】:

我有一个通用的 React 函数组件,其中 T extends ReactText | boolean,它的 props 包含一个返回 T 的方法。

import React, { FC, ReactText } from 'react';

interface Thing {}

interface Props<T extends ReactText | boolean> {
  value: T | null;
  mapValues(thing: Thing): T;
}

interface MyComponent<T extends ReactText | boolean> extends FC<Props<T>> {}

function MyComponent<T extends ReactText | boolean>(
  { value, mapValues }: Props<T>
) {
  // do stuff
  return <div />;
}

但是,我想强制该方法返回 ReactTextboolean 之一;它也不能是返回的函数。

import React, { FC, ReactText } from 'react';

interface Thing {
  someBool: boolean;
  someStr: string;
}

interface Props<T extends ReactText | boolean> {
  value: 
    boolean extends T ? boolean | null : 
    ReactText extends T ? ReactText | null : 
    never;

  mapValues(thing: Thing): 
    boolean extends T ? boolean : 
    ReactText extends T ? ReactText :
    never;

  things: Thing[];
}

interface MyComponent<T extends ReactText | boolean> extends FC<Props<T>> {}
function MyComponent<T extends ReactText | boolean>(
  { value, mapValues, things }: Props<T>
) {
  if (value != null) {
    return <div>{value}</div>;
  }

  // super simplified verison of what I'm actually doing
  const items = things
    .map(mapValues)
    .map(v => <li key={v.toString() /** assume v is unique */}>{v}</li>);

  return <ul>{items}</ul>;
}

虽然非常丑陋,但它确实有效(在 TS Playground 中写出来后让我很惊讶)。

const things: Thing[] = [
  { someStr: 'a', someBool: false },
  { someStr: 'b', someBool: true }
];

const value = null;
const mapValues = (thing: Thing) => 
  things.find(t => t.someStr === thing.someStr)?.someStr ?? 
  things.find(t => t.someBool === thing.someBool)?.someBool ??
  false;

// succeeds but is ugly and potentially fragile
// @ts-expect-error
<MyComponent things={things} value={value} mapValues={mapValues} />;

// this one works
// I don't know why `as ReactText` is needed when `ReactText` is `string | number`.
<MyComponent things={things} value={'abc' as ReactText} mapValues={() => 'abc' as ReactText} />;
Type '(thing: Thing) => string | boolean' is not assignable to type '(thing: Thing) => boolean'.  
  Type 'string | boolean' is not assignable to type 'boolean'.  
    Type 'string' is not assignable to type 'boolean'.

不过,我无法想象这是最好的解决方案。它很冗长,感觉很脆弱。

是否存在不是based on interface propertiesXOR 类型,因为这在这种情况下不起作用?也许使用Extract&lt;T, U&gt;

TS Playground

【问题讨论】:

  • 您能否展示一些应该有效的用法示例(而不仅仅是那些出错的示例),以便我们测试解决方案是否支持您的用例?
  • @jcalz 我更新了操场和代码 sn-p。
  • this 是否满足您的需求?玩弄它,让我知道。
  • @jcalz 是的,这确实有效。谢谢!
  • 好的,有机会我可以写一个答案。

标签: typescript


【解决方案1】:

您遇到的情况是泛型类型参数不能轻易地将constrained 设置为一组类型中的一个。看 microsoft/TypeScript#27808 获取相关功能请求。

假设您有一个类型参数T 和三个类型ABC,并且您想将T 限制为这三种类型之一。看似显而易见的方法T extends (A | B | C) 是不够的。它允许你想允许的一切,但它不禁止你想禁止的一切。例如,它允许A | B,因为(A | B) extends (A | B | C),即使(A | B) extends A(A | B) extends B 通常都不是真的。

您确实需要像 T extends_oneof {A, B, C}(T extends A) | (T extends B) | (T extends C) 这样的约束,但这不是有效的 TypeScript,并且没有直接的替代方案。

在你的情况下,你想要T extends_oneof {ReactText, boolean},但你只有T extends ReactText | boolean,它允许你不想要的东西,比如string | true


所以我们必须解决它。我们可以构建一个名为OneOf&lt;T, C&gt; 的类型函数,它接受一个类型T 和一个约束元组C,并且当且仅当它扩展Cnever 的成员之一时才返回T除此以外。这是一种方法:

type OneOf<T extends C[number], C extends readonly any[]> =
  Extract<{ [I in keyof C]-?: [T] extends [C[I]] ? T : never }[number], T>;

type O = OneOf<"abc", [string, number, boolean]> // "abc"
type P = OneOf<123, [string, number, boolean]> // 123
type Q = OneOf<"abc" | 123, [string, number, boolean]> // never

如果您不关心对任意长度的约束元组进行这项工作,则可以为一对约束构建等效函数:

type OneOfTwo<T extends C1 | C2, C1, C2> =
  [T] extends [C1] ? T : [T] extends [C2] ? T : never;

这可能更容易理解。在这两种情况下,我都使用[T] extends [XXX] 而不是T extends X 来防止支票成为distributive in unions,这会破坏支票的目的。


无论如何,如果你能写出你的类型来将T 限制为OneOf&lt;T, C&gt;,那就太棒了:

interface IWish<T extends OneOf<T, [string, number, boolean]>> { // error!
  // -------------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Type parameter 'T' has a circular constraint.(2313)
  a: T
}

但这是一个非法的循环约束。没有这样的运气。所以OneOf&lt;T, C&gt; 的任何使用都需要在约束之外;可能在其他类型的位置代替T。这在实践中很容易,但它仍然是一个限制,使它更像是一种变通方法而不是解决方案。

如果传递给OneOf&lt;T, C&gt; 的无效T 代替never 会产生“无效类型”,这样编译器就会在那里发出错误,那也很好。请参阅microsoft/TypeScript#23689 了解 相关的功能请求。但同样,没有这样的运气。这意味着任何使用OneOf&lt;T, C&gt; 都将依赖never 导致错误。这在实践中很容易,但同样是解决方法。


好的,足够的警告。让我们看看它的实际效果。我建议不要管你的 Props 定义,只担心在你的 MyComponent 函数中限制 T ......就像这样:

function MyComponent<T extends ReactText | boolean>(
  props: Props<OneOf<T, [ReactText, boolean]>>
) {

  const { value, mapValues, things } = props as Props<T>;

  // ...    
}

请注意,在函数内部,我从Props&lt;OneOf&lt;T, [ReactText, boolean]&gt;&gt;props 断言为Props&lt;T&gt;,因为前者在函数内部更容易操作;依赖于未解析的泛型类型参数的条件类型很难处理。

让我们测试调用方(我使用常规调用样式而不是 JSX 样式,因此我们可以手动指定和/或检查类型参数):

MyComponent({ things, value: "abc", mapValues: () => "def" }); // okay
MyComponent({ things, value: true, mapValues: () => false }); // okay
MyComponent({ things, value: true, mapValues: () => "def" }); // error
// Type '"def"' is not assignable to type 'true' -> ~~~~~
MyComponent<boolean | ReactText>(
  { things, value: null, mapValues: () => 123 }); // error
// -------------------------------------> ~~~
// Type 'number' is not assignable to type 'never' 

看起来不错!允许的呼叫按需要工作。不允许的呼叫也根据需要被禁止。在最后一次调用中,您可以看到我们为T 手动指定了boolean | ReactText,这导致组件需要为Props&lt;never&gt; 类型,这不起作用,因为mapValues() 没有返回never


Playground link to code

【讨论】:

    【解决方案2】:

    (编辑:我发现booleanReactText 本身都是联合类型,这使得该解决方案对您的用例有问题)

    对于联合类型,该类型将评估为 never

    type NoUnionTypes<T> = [T] extends infer TTuple // keeps T "together"
      // distributive conditional, blows T apart if it's a union
      ? T extends infer U  
        ? TTuple extends [U] ? T : never 
        : never 
      : never;
    

    (可能有更好的名字)

    使用这种类型,我们可以像这样编写你的Props 接口:

    interface Props<T extends ReactText | boolean> {
      value: NoUnionTypes<T> | null;
      mapValues(thing: Thing): NoUnionTypes<T>;
      things: Thing[];
    }
    

    Link to TS Playground

    【讨论】:

    • boolean 本身就是一个联合体,所以可能会发生不好的事情
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-22
    • 1970-01-01
    • 2019-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多