打字稿参数相互依赖

时间:2020-07-17 17:47:37

标签: typescript generics typescript-typings

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

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

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 })
}

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

1 个答案:

答案 0 :(得分:1)

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

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"。编译器将a视为类型"T" | "U",将b视为类型"X" | "Y",两者都是正确的。但是这样做无法跟踪它们之间的相关性。因此[a, b]被认为是类型["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()fooRight())或使用type assertions来使编译器静音:

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()fooAssert()的呼叫签名与fooEither()中的呼叫签名是相同的不安全密钥。在这种情况下,如果我是您,我可能会退回到类似fooSpread()的位置,至少可以安全地致电。


相关值的问题经常出现,以至于我提出了一个问题,microsoft/TypeScript#30581,这表明最好在这里提供比复制或断言更好的东西。 las,没有明显的结果。


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

Playground link to code