TypeScript联合函数类型

时间:2019-10-30 16:01:37

标签: typescript

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

理想情况下,我希望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'
    }
});

谢谢!

2 个答案:

答案 0 :(得分:2)

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

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不能执行任何操作,First仅被称为Second never。如果尝试调用它,无论如何都会收到错误消息。函数的 union 只能安全地对其参数的 intersection 起作用。 added in TS3.3对此提供了一些支持,但是在这种情况下,参数类型是互斥的,因此只有可接受的参数类型为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 } 是不可调用的。

我怀疑这种相互不兼容的功能的结合是否是任何人想要的。联合和交集的对偶性以及函数类型的contravariance就其参数类型而言是令人困惑且难以谈论的,但是区别很重要,因此我觉得值得为此着迷。这种联合就像是发现我必须安排与谁会在周一 或于周二开会的人开会,但我不知道。我想如果我可以在星期一星期二都开会,那会奏效,但是假设那没有意义,那我就停滞了。我要见的人是一个工会,而我要见的那一天是一个十字路口。做不到。


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

x

现在我们知道First既是Second 又是First。您会发现您可以将其像Secondx(1, "second")那样对待并且很好。您不能像// unfortunately we still have the implicitAny problem: intersectionTest((x, y) => { }) // error! x, y implicitly any 那样将其视为一些奇怪的混合体,但是大概就是您想要的。现在,我正计划与在 星期一星期二有空的人开会。如果我问那个人安排会议的日期,她可能会说“星期一或星期二都适合我”。会议的那天是工会,而我要开会的人是一个十字路口。可行。


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

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
}

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

intersectionTest()

就其行为而言,它与any相同,但是现在参数具有已知的类型,并且可以在上下文中键入比equivalentToIntersectionTest((y, z) => { // y is string | number // z is 'first' | 'second' // relationship gone if (z === 'first') { y.toFixed(); // error! } }) 更好的类型:

(y, z) => {...}

不幸的是,如上所述,如果使用y实现该回调,则zequivalentToIntersectionTest((...yz) => { // yz is [number, "first"] | [string, "second"], relationship preserved! 的类型将成为独立的并集。编译器会忘记它们彼此相关。一旦将参数列表视为单独的参数,就会失去相关性。我已经见过足够多的问题,需要我filed an issue about it来解决此问题,但目前尚无直接支持。

让我们看看如果不立即将参数列表分开,将剩余参数散布到数组中并使用它,会发生什么情况:

yz

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


这里的下一步是尝试通过类型保护测试将yz缩小到联合的一个或另一分支。最简单的方法是ydiscriminated union。而且它是 ,但不是因为yz[0](或number)。 string if (typeof yz[0] === "number") { yz[1]; // *still* 'first' | 'second'. } 不是文字类型,不能直接用作判别式:

yz[0]

如果必须检查z,则必须实现自己的type guard function以支持该检查。相反,我建议您打开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进行比较之后,y的类型不再是并集,因此您可以将其分解为z和{{1 }}。


好的,呼呼。好多啊。 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

答案 1 :(得分:0)

似乎无法使用当前的TS工具来实现该结果,但是如果将参数作为单个对象提供,则可以实现类似的效果。尽管在typeof上进行y检查仍然不能缩小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('');
    }
});