TypeScript 联合函数类型

Gra*_*fus 7 typescript

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

理想情况下,我想y成为类型number | string,并且z成为'first' | 'second'。但是在缩小了yorz的类型后,会自动推断出另一个参数的缩小类型。

不幸的是,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'
    }
});
Run Code Online (Sandbox Code Playgroud)

谢谢!

jca*_*alz 9

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

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

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

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'.
}
Run Code Online (Sandbox Code Playgroud)

unionTest()功能与您的 相同test(),但它不能对 执行任何操作x,而只知道是 aFirst a Second。如果你尝试调用它,无论如何你都会得到一个错误。函数的联合只能安全地作用于它们的参数的交集在 TS3.3添加了对此的一些支持,但在这种情况下,参数类型是互斥的,所以只有可接受的参数是类型never......所以x是不可调用的。

我怀疑这种相互不兼容的功能的联合是任何人想要的。并集和交集的二元性以及函数类型相对于其参数类型的逆变性令人困惑且难以谈论,但区别很重要,因此我觉得这一点值得详细讨论。这个联盟就像是找出我有安排与别人谁就会开会或者可在周一将可在周二,但我不知道是哪个。我想如果我能在星期一星期二举行会议,那会奏效,但假设这没有意义,我就被卡住了。我会见的人是工会,而我会见的那天是十字路口。做不到。


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

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
}
Run Code Online (Sandbox Code Playgroud)

现在我们知道x它既是 aFirst 是 a Second。你可以看到你可以把它当作 aFirst或 a来对待Second。但是,您不能将它视为某种奇怪的混合体,例如x(1, "second"),但大概这就是您想要的。现在我正在安排与周一周二都有空的人会面。如果我问那个人在哪一天安排会议,她可能会说“周一或周二我都可以”。开会那天是一个联合,而我会见的人是一个交集。那个有效。


所以现在我假设您正在处理函数的交集。不幸的是,编译器不会自动为您合成参数类型的并集,您最终仍会遇到“隐含任何”错误。

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

您可以手动将函数的交集转换为作用于参数类型联合的单个函数。但是对于两个受约束的参数,表达这一点的唯一方法是使用rest arguments 和 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
}
Run Code Online (Sandbox Code Playgroud)

这与intersectionTest()它的行为方式相同,但现在参数具有已知的类型,并且可以在上下文中输入比any以下更好的类型:

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

不幸的是,你看上面,如果实现与回调(y, z) => {...}的类型y,并z成为独立工会。编译器忘记了它们是相互关联的。一旦您将参数列表视为单独的参数,就会失去相关性。我已经看到足够多的问题想要解决这个问题,我为此提出了一个问题,但目前没有直接支持。

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

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

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


下一步是尝试yz通过类型保护测试缩小到联合的一个或另一条腿。最简单的方法是 ifyz是一个受歧视的联合。它,但不是因为y(或yz[0])。 number并且string不是文字类型,不能直接用作判别式:

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

如果您必须检查yz[0],则必须实现自己的类型保护功能来支持它。相反,我建议打开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
  }
});
Run Code Online (Sandbox Code Playgroud)

请注意,在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
  }
});
Run Code Online (Sandbox Code Playgroud)

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

代码链接