首页 > 解决方案 > 打字稿参数相互依赖

问题描述

我不明白我在下面遇到的错误

这是一个最小的可重现示例,带有错误消息

type LeftChild = {
    element: 0
}

type RightChild = {
    element: 1
}

type Left = {
    child: LeftChild
}

type Right = {
    child: RightChild
}

interface Typings {
    "left": {
        parent: Left
        child: LeftChild
    }
    "right": {
        parent: Right
        child: RightChild
    }
}

function foo<T extends keyof Typings>(parents: Typings[T]["parent"][], child: Typings[T]["child"]) {
    // Works
    parents[0].child = child

    // Doesn't work
    parents.push({ child })
    /*           ^^^^^^^^^
                Argument of type '{ child: Typings[T]["child"]; }' is not assignable to parameter of type 'Typings[T]["member"]'.
                Type '{ child: Typings[T]["child"]; }' is not assignable to type 'Left | Right'.
                    Type '{ child: Typings[T]["child"]; }' is not assignable to type 'Right'.
                    Types of property 'child' are incompatible.
                        Type 'Typings[T]["child"]' is not assignable to type 'RightChild'.ts(2345)
    */
}

复制粘贴两种类型的实现时,它工作正常

function fooLeft(parents: Left[], child: LeftChild) {
    parents[0].child = child
    parents.push({ child })
}
function fooRight(parents: Right[], child: RightChild) {
    parents[0].child = child
    parents.push({ child })
}

什么是适合我的功能的类型?我正在尝试使用泛型让函数在几种类型上运行

标签: typescriptgenericstypescript-typings

解决方案


这里发生了很多事情;简短的回答是,面对相关的联合类型表达式,编译器确实没有能力验证类型安全。考虑这段代码:

declare const [a, b]: ["T", "X"] | ["U", "Y"]; // okay
const oops: ["T", "X"] | ["U", "Y"] = [a, b]; // error!

这里变量ab是相关的:如果a"T"b必须是"X"。否则a"U"b"Y"。编译器认为atype"T" | "U"bas type"X" | "Y"都是正确的。但是通过这样做,它未能跟踪它们之间的相关性。所以[a, b]被认为是 type ["T", "X"] | ["T', "Y"] | ["U", "X"] | ["U", "Y"]。你会得到错误。


这或多或少是发生在你身上的事情。我可以将您的代码重写为这样的非通用版本:

function fooEither(parents: Left[] | Right[], child: LeftChild | RightChild) {
  parents[0].child = child; // no error, but unsafe!
  parents.push({ child }) // error
}
fooEither([{ child: { element: 0 } }], { element: 1 }); // no error at compile time

在这里,您可以看到编译器对.感到满意parents[0].child = child,但它不应该... parents.push({ child })没有什么可以限制parentschild正确关联。

为什么parents[0].child = child有效?好吧,编译器在很大程度上确实允许不合理的属性写入。例如,请参阅microsoft/TypeScript#33911,以及关于他们为什么决定保持这种方式的讨论。

我可以尝试重写上面的代码以在调用站点强制执行相关性,但编译器仍然无法在实现中看到它:

function fooSpread(...[parents, child]: [Left[], LeftChild] | [Right[], RightChild]) {
  parents[0].child = child; // same no error
  parents.push({ child }) // same error
}
fooSpread([{ child: { element: 0 } }], { element: 1 }); // not allowed now, that's good

如果编译器没有某种方式来维护联合类型之间的相关性,那么这里就没有什么可做的了。


我在这里看到的两种解决方法是复制代码(很像fooLeft()and fooRight())或使用类型断言来使编译器静音:

function fooAssert<T extends keyof Typings>(parents: Typings[T]["parent"][], child: Typings[T]["child"]) {
  parents[0].child = child
  parents.push({ child } as Typings[T]["parent"]); // no error now
}

这可以编译,但只是因为有责任告诉编译器您正在做的事情是安全的,而不是相反。这很危险,因为断言使您更有可能在没有意识到的情况下做了不安全的事情:

fooAssert([{ child: { element: 0 } }], { element: 1 }); // look ma no error
// fooAssert<"left"|"right">(...);

哎呀,怎么了?好吧,您的调用签名是通用的,T但没有T传入类型的参数。编译器无法推断出任何内容T,而是将其扩大到其约束,即"left" | "right". foo()因此and的调用签名fooAssert()fooEither(). 在这种情况下,如果我是你,我可能会退回到fooSpread()至少可以安全调用的东西。


相关值的问题经常出现,以至于我提交了一个问题,microsoft/TypeScript#30581,表明在这里有比重复或断言更好的东西会很好。唉,没有什么明显的事情即将发生。


好的,希望有帮助;祝你好运!

Playground 代码链接


推荐阅读