【问题标题】:TypeScript union function typesTypeScript 联合函数类型
【发布时间】:2020-02-25 23:48:00
【问题描述】:

我正在尝试为箭头函数创建联合类型。目标是能够根据第一个参数推断第二个参数的类型。

理想情况下,我希望y 的类型为number | string,而z 的类型为'first' | 'second'。但是在缩小yz的类型后,自动推断另一个参数的缩小类型。

不幸的是,TypeScript 似乎无法处理这样的复杂案例,但我想知道你们中是否有人遇到过类似的问题。

在一个简化的场景中,我的代码如下所示:

type Callback<T1, T2> = (y: T1, z: T2) => void;

const test = (x: Callback<number, 'first'> | Callback<string, 'second'>) => {
    return;
}

// Parameter 'y' implicitly has an 'any' type.
// Parameter 'z' implicitly has an 'any' type.
test((y, z) => {
    if(typeof y === 'number') {
        // y is a number
        // so z must be 'first'
    } else {
        // y is a string
        // so z must be 'second'
    }
});

谢谢!

【问题讨论】:

  • 您希望test 接受要么 Callback&lt;number, 'first'&gt; 还是 Callback&lt;string, 'second'&gt;?这就是你输入它的方式,但我怀疑这就是你的意思,因为这样的回调不可能安全地调用。或者你想让test 接受一个回调,它本身可以作为任何一种类型,这意味着它是both?这更像是&amp; 而不是|
  • 我想接受/或,这就是我使用|的原因。
  • 那么,假设test 的实现不只是return,你希望x 做什么?
  • 或者,换句话说,您需要 y 类似于 number &amp; string,而不是 number | string。这没有多大意义,所以我质疑x 的输入。见this feature;函数类型的联合只能通过参数的交集来调用。
  • 使用type Callback = (...args: [number, 'first'] | [string, 'second']) =&gt; void 几乎可以做到。这样,“隐式任何”错误就消失了,但是如果 y 是数字,编译器仍然不知道 z 必须是“第一”。

标签: typescript


【解决方案1】:

这就是我所看到的情况。让我们使用这些定义:

type Callback<T1, T2> = (y: T1, z: T2) => void;
type First = Callback<number, 'first'>;
type Second = Callback<string, 'second'>;

首先,我绝对怀疑您想要函数的联合而不是函数的交集。请注意,这样的函数联合本质上是无用的:

const unionTest = (x: First | Second) => {
  // x is *either* a First *or* it is a Second, 
  // *but we don't know which one*.  So how can we ever call it?

  x(1, "first"); // error! 
  // Argument of type '1' is not assignable to parameter of type 'never'.
  x("2", "second"); // error!
  // Argument of type '"2"' is not assignable to parameter of type 'never'.
}

unionTest() 函数与您的 test() 相同,但它不能与 x 做任何事情,x 只知道是 First @987654336 @。如果您尝试调用它,无论如何都会出错。 union 函数只能安全地作用于其参数的交集。对此的一些支持是added in TS3.3,但在这种情况下,参数类型是互斥的,因此只有可接受的参数类型为never...所以x 是不可调用的。

我怀疑这种相互不兼容的功能的结合是否是任何人想要的。并集和交集的对偶性以及函数类型的contravariance 相对于它们的参数类型令人困惑且难以谈论,但区别很重要,所以我觉得这一点值得深究。这个工会就像发现我必须安排与某人会面,该人要么周一有空周二有空,但我不知道哪个.我想如果我可以在星期一 星期二举行会议,那将是可行的,但假设这没有意义,我就被困住了。与我会面的人是工会,而我会面的那一天是一个十字路口。做不到。


相反,我认为您想要的是函数的交集。这对应于overloaded 函数;你可以用两种方式称呼它。看起来像这样:

const intersectionTest = (x: First & Second) => {
  // x is *both* a First *and* a Second, so we can call it either way:
  x(1, "first"); // okay!
  x("2", "second"); // okay!
  // but not in an illegal way:
  x(1, "second"); // error, as desired
  x("2", "first"); // error, as desired
}

现在我们知道x 既是First,又是 Second。你可以看到你可以把它当作FirstSecond 来对待并且没问题。但是,您不能将其视为某种奇怪的混合体,例如x(1, "second"),但大概这就是您想要的。现在我正在安排与某人的会议,他将在 周一 周二有空。如果我问那个人在哪一天安排会议,她可能会说“星期一或星期二我都可以”。见面的那天是工会,和我见面的人是十字路口。这行得通。


所以现在我假设您正在处理函数的交集。不幸的是编译器doesn't automatically synthesize the union of parameter types for you,你仍然会得到那个“隐式任何”错误。

// unfortunately we still have the implicitAny problem:
intersectionTest((x, y) => { }) // error! x, y implicitly any

您可以手动将函数的交集转换为作用于参数类型联合的单个函数。但是对于两个受约束的参数,表达这一点的唯一方法是使用rest arguments and rest tuples。我们可以这样做:

const equivalentToIntersectionTest = (
  x: (...[y, z]: Parameters<First> | Parameters<Second>) => void
) => {
  // x is *both* a First *and* a Second, so we can call it either way:
  x(1, "first"); // okay!
  x("2", "second"); // okay!
  // but not in an illegal way:
  x(1, "second"); // error, as desired
  x("2", "first"); // error, as desired
}

就其行为方式而言,这与intersectionTest() 相同,但现在参数具有已知类型并且可以根据上下文键入比any 更好的类型:

equivalentToIntersectionTest((y, z) => {
  // y is string | number
  // z is 'first' | 'second'
  // relationship gone
  if (z === 'first') {
    y.toFixed(); // error!
  }
})

不幸的是,正如您在上面看到的,如果您使用(y, z) =&gt; {...} 实现该回调,则yz 的类型将成为独立的联合体。编译器忘记了它们是相互关联的。一旦您将参数列表视为单独的参数,您就会失去相关性。我已经看到足够多的问题想要解决这个问题,我 filed an issue about it,但目前还没有直接的支持。

让我们看看如果我们不立即分离参数列表会发生什么,通过将其余参数分散到一个数组中并使用它:

equivalentToIntersectionTest((...yz) => {
  // yz is [number, "first"] | [string, "second"], relationship preserved!

好的,那很好。现在yz 仍在跟踪约束。


这里的下一步是尝试通过类型保护测试将yz 缩小到工会的一个或另一个分支。最简单的方法是如果yzdiscriminated union。它,但不是因为y(或yz[0])。 numberstring 不是字面量类型,不能直接用作判别式:

  if (typeof yz[0] === "number") {
    yz[1]; // *still* 'first' | 'second'.  
  }

如果你必须检查yz[0],你必须实现你自己的type guard function 来支持它。相反,我建议打开z(或yz[1]),因为"first""second" 是可用于区分联合的字符串文字:

  if (yz[1] === 'first') {
    // you can only destructure into y and z *after* the test
    const [y, z] = yz;
    y.toFixed(); // okay
    z === "first"; // okay
  } else {
    const [y, z] = yz;
    y.toUpperCase(); // okay
    z === "second"; // okay
  }
});

请注意,在将yz[1]'first' 进行比较后,yz 的类型不再是联合,因此您可以以更有用的方式解构为yz


好的,哇。好多啊。 TL;DR 代码:

const test = (
  x: (...[y, z]: [number, "first"] | [string, "second"]) => void
) => { }

test((...yz) => {
  if (yz[1] === 'first') {
    const [y, z] = yz;
    y.toFixed();
  } else {
    const [y, z] = yz;
    y.toUpperCase(); // okay
  }
});

希望有所帮助;祝你好运!

Link to code

【讨论】:

  • 感谢冗长的解释,真的很有用!很高兴您提出问题,对此的支持将非常方便。作为旁注,TL;DR 代码与我发布的代码基本相同,只是一个元组 args 而不是一个对象。我想这样我可以把论点分开很方便。 :) 再次感谢!
【解决方案2】:

似乎使用当前的 TS 工具无法实现该结果,但是如果将参数作为单个对象提供,则可以实现类似的效果。尽管对y 进行typeof 检查仍然不会缩小z 的类型。

type Test<T1, T2> = {
    y: T1;
    z: T2;
};

const test = (x: (args: Test<number, 1> | Test<string, 'second'>) => void) => {
    return;
}

test((args) => {
    if(args.z === 1) {
        // args.y recognized as number
        args.y.toExponential();
    } else {
        // args.y recognized as string
        args.y.split('');
    }
});

【讨论】:

  • 请注意(args: Test&lt;number, 1&gt; | Test&lt;string, 'second'&gt;) =&gt; void 不等同于((args: Test&lt;number, 1&gt;)=&gt;void) | ((args: Test&lt;string, 'second'&gt;)=&gt;void)。函数的参数类型为contravariant,所以函数的并集对应于函数参数的交集,反之亦然。
  • 我知道它们是不等价的,第二种情况的机器人 TypeScript 只会推断出 any 类型。这就是为什么这个答案只是我能得到的最接近的答案,而不是对我的问题的 1:1 回复。
猜你喜欢
  • 2021-04-19
  • 2019-02-08
  • 1970-01-01
  • 2019-04-20
  • 1970-01-01
  • 1970-01-01
  • 2021-02-20
  • 2021-10-27
  • 1970-01-01
相关资源
最近更新 更多