typescript - 将数据字段与导致错误的可区分联合链接起来

标签 typescript typescript2.2

我正在开发一个 Ionic (3.0.0) 应用程序,经常想在数据接口(interface) 中链接两个字段的类型。例如,一个 NotificationDataverb: 'mention' | '分享' | ...引用:ProfileReference |文档引用 | ... 字段,但实际上,NotificationData 是联合类型:

{ verb: 'mention', reference: ProfileReference } | { verb: 'share', reference: DocumentReference }

到目前为止,还不错。还有其他字段不随 verb 改变,所以我通常创建一个基本接口(interface),以不同的方式扩展它,然后采用联合,就像这样:

type X = 'a' | 'b' | 'c';
type Y = 'd' | 'e' | 'f';

interface I { x: X, other: Y };
interface A extends I { x: 'a', other: 'd' };
interface B extends I { x: 'b', other: 'e' };
interface C extends I { x: 'c', other: 'f' };
type J = A | B | C;

只要我对数据进行硬编码就可以了

const j: J = { x: 'a', other: 'd' } // OK

或者从一个开关

生成一个整体
function f(x: X): J {
  switch (x) {
    case 'a': return { x, other: 'd' };
    case 'b': return { x, other: 'e' };
    case 'c': return { x, other: 'f' };
    default: ((y: never) => {})(x);
  }
}
// OK

但如果我尝试以其他方式生成它,Typescript 会提示:

function other(x: X): Y {
  switch (x) {
    case 'a': return 'd';
    case 'b': return 'e';
    case 'c': return 'f';
    default: ((y: never) => {})(x);
  }
}

function g(x: X): J {
  return { x, other: other(x) }
}
// Error:
// Type '{ x: X; other: number; }' is not assignable to type Z.
//   Type '{ x: X; other: number; }' is not assignable to type 'C'.
//     Types of property 'x' are incompatible.
//       Type 'X' is not assignable to type '"c"'.
//         Type '"a"' is not assignable to type '"c"'.

事实上,即使没有数据字段链接也会出现这些错误:

interface I2 { x: X, other: any };
interface A2 extends I2 { x: 'a' };
interface B2 extends I2 { x: 'b' };
interface C2 extends I2 { x: 'c' };
type J2 = A2 | B2 | C2;

function h(x: X): J2 { return { x, other: 0 }; }
// Error:
// Type '{ x: X; other: number; }' is not assignable to type J2.
//   Type '{ x: X; other: number; }' is not assignable to type 'C2'.
//     Types of property 'x' are incompatible.
//       Type 'X' is not assignable to type '"c"'.
//         Type '"a"' is not assignable to type '"c"'.

我可以在我的类型签名中使用 I

const xs: X[] = ['a', 'b', 'c'];
const is: I2[] = xs.map(x => ({ x, other: 0 })) // OK

但这失去了我最初想要的字段链接。我也可以总是像上面的 function f 一样使用 switch,例如

const js: J[] = xs.map(f); // OK

但我希望能够在不创建单独函数的情况下执行此操作,例如

const j2s: J2[] = xs.map(x => ({ x, other: 0 }))
// Error: ... Type '"a"' is not assignable to type '"c"'.

无论如何,这感觉像是 Typescript 应该能够表达/处理的东西。

所以我的问题是,是否有更好的方法来在 Typescript 中表达这种链接字段类型信息?或者另一种从过程中生成 J[] 的方法X[]?

最佳答案

这是 TypeScript 类型系统的限制。我将简化您问题中的代码,以便更容易发现问题:

function f(x: 'a' | 'b' | 'c'): { x: 'a' } | { x: 'b' } | { x: 'c' } {
    return { x: x };
}

// Type '{ x: "a" | "b" | "c"; }' is not assignable to type '{ x: "a"; } | { x: "b"; } | { x: "c"; }'.

这个错误信息比较容易理解。类型检查器看到您使用类型为 'a' | 的值。 'b' | 'c' 作为对象中的 x 条目,并推断出 { x: 'a' | 的类型'b' | 'c' } 用于表达式。这与 { x: 'a' } | 不同{ x: 'b' } | {x:'c'}!特别是,类型检查器不理解 'a' | 类型的每个可能值。 'b' | 'c' 实际上会生成类型为 { x: 'a' } | 的有效对象{ x: 'b' } | { x: 'c' }.

所以编译器需要一些帮助。我们可以通过在构造对象之前执行案例分析来取得成功:

function f(x: 'a' | 'b' | 'c'): { x: 'a' } | { x: 'b' } | { x: 'c' } {
    switch (x)
    {
        case 'a': return { x: x };
        case 'b': return { x: x };
        case 'c': return { x: x };
    }
}

case 的每个分支中,x 的类型被缩小(到 'a''b''c'),因此返回的表达式类型为 { x: 'a' }, { x: 'b' }{ x: 'c' }。这些显然在 { x: 'a' } | 处进行类型检查{ x: 'b' } | { x: 'c' }.

如果您真的无法承受 switch 语句所需的额外击键,我认为最简单和最实用的解决方法就是暂时关闭类型系统。

function f(x: 'a' | 'b' | 'c'): { x: 'a' } | { x: 'b' } | { x: 'c' } {
    return <any>{ x: x };
}

您可能有理由反对 TypeScript 应该能够告诉 { x: 'a' | 'b' | 'c' } 可分配给 { x: 'a' } | { x: 'b' } | {x:'c'}。像编译器编写者一样思考这个问题:检查此类类型的算法会是什么样子?

  1. 分解联合类型'a' | 'b' | 'c' 转换为它的构成文字类型。
  2. 分解结果类型 { x: 'a' } | { x: 'b' } | { x: 'c' } 到它的构成对象类型中。
  3. 对于每一对类型({x: x}resultType),比较类型以查看它们是否可分配。

在伪代码中:

foreach (t: Type in 'a' | 'b' | 'c')
{
    var checks = false;
    foreach (u: Type in { x: 'a' } | { x: 'b' } | { x: 'c' })
    {
        if ({ x: t } :<= u)
        {
            checks = true;
        }
    }
    if (!checks)
    {
        return false;
    }
}
return true;

我们已经编写了一个 O(n^2) 类型检查算法!另一种方法可能是将类型放入乘积总和范式中 - 这将允许您进行比较,例如 { x: { y: 'a' | 'b' } | { y: 'c' | 'd' } } { x: { y: 'a' } } | { x: { y: 'b' } } | { x: { y: 'c' } } | { x: { y: 'd' } } - 但是 the normalisation algorithm blows up exponentially , 你仍然无法处理递归类型的同义词。

这个故事的寓意是,在实践中,类型系统不仅仅是集合论。仅仅因为两种类型包含相同的一组值并不意味着您应该期望它们被机器判断为相等。在最先进的类型系统中——依赖类型系统——类型检查器通常需要对程序的可靠性进行计算证明。 (例如,您可能知道数组的长度为 n + m,但如果类型检查器需要一个长度为 m + n 的数组,则您必须编写额外的代码让机器相信你已经履行了你的义务。)

关于typescript - 将数据字段与导致错误的可区分联合链接起来,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43743657/

相关文章:

typescript - DynamoDB 表查询 KeyConditionExpression 错误

javascript - 如何获取特定列并在 dynamoDB 中强制执行条件?

用大括号包裹的 typescript 返回值?

arrays - 将数组传递给异步库 eachSeries - 期望 'Dictionary<{}>'

javascript - 为什么 TS 提示函数体内的函数声明

javascript - Typescript 和 Angular : Type a string to a component class

javascript - TypeScript 闭包——一个 "almost"的解决方案

javascript - typescript 错误 : Property 'append' does not exist on type 'HTMLElement'

javascript - 使用在 TypeScript 的其他模块中声明的别名调用函数