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

Linking data fields with discriminated unions causing errors

我正在开发 Ionic (3.0.0) 应用程序,并且经常想要 link 数据中两个字段的类型 interface。例如,NotificationDataverb: 'mention' | 'share' | ...reference: ProfileReference | DocumentReference | ... 字段,但实际上,NotificationData 是联合类型:

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

到目前为止,还不错。 verb还有一些不改变的字段,所以我通常创建一个基本接口,以不同的方式扩展它,然后取并集,像这样:

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

或从 switch

生成一个整体
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"'.

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

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

但这失去了我最初想要的字段 linking。我也可以像上面的 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 应该能够做到的express/handle。

所以我的问题是,有没有更好的方法来表达这个 linked-field type information in 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,并且您仍然将无法处理递归类型的同义词。

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