具有多个区分键的Typescript联合类型

时间:2019-10-25 15:21:02

标签: typescript

问题陈述:

当创建由具有不同字段的对象组成的并集类型时,其中某些类型包含多个区分键,Typescript不能确定哪种类型是错误消息的正确类型。

我确定我做错了什么,但是我试图到达一个错误消息对于此函数调用直观的地方。

代码:

interface A1 {
  name: "A";
  type: 1;
  location: "some" | "string" | "literal" | "types";
  subType: "more" | "string" | "literal" | "types";
}

interface A2 {
  name: "A";
  type: 2;
  location: "some" | "string" | "literal" | "types" | "can" | "be" | "shared";
  subType: "not" | "shared";
}

interface B {
  name: "B";
  type: 1;
  location: "could" | "still" | "be" | "shared";
  subType: "probably" | "not";
}

interface C {
  name: "C";
  notype: "field" | "test";
}

type Events = A1 | A2 | B | C;

async function trackIt({ name, ...payload }: Events): Promise<boolean> {
  // do somethng cool.
  return true;
}

trackIt({
  name: "A",
  type: 2,
  location: "some",
  subType: "not a correct literal"
});

沙箱:

https://codesandbox.io/s/competent-pond-918m1?fontsize=14

不正确的“ trackIt”调用所需的错误消息:

““非正确文字”不能应用于“非” |“共享””(正确标识我们正在处理A2类型,但subType不正确。)

实际错误消息:

“类型'{的参数:名称:“ A”;类型:2;位置:“ some”;子类型:“不是正确的文字”;}'无法分配给类型'事件'的参数。   输入'{name:“ A”;类型:2;位置:“一些”; subType:“不正确的文字”; }”不可分配给类型“ B”。     属性“名称”的类型不兼容。       不能将类型“ A”分配给类型“ B”。

1 个答案:

答案 0 :(得分:1)

TypeScript并不能真正让您自定义错误。有open suggestion允许类似Invalid<T>之类的东西在每当对某种类型求值时都会生成编译器错误,并输出类型T作为消息。不幸的是,这还没有实现,可用的解决方法非常难看。

您在这里面临的一般问题似乎是编译器无法找出(从开发人员的角度来看)导致类型检查失败的最相关原因。如果您有一个应该但不能分配给像X | Y | Z这样的联合类型的值,则编译器必须确定要投诉的成员中的哪一个。它使用某种启发式方法,但总是存在边际情况。我看到另一个open suggestion可以通过选择“最接近”的成员以某种方式进行改进,但是它并没有详细说明“最接近”的含义(因此问题需要提出建议),并且在任何情况下都不属于该提议的一部分。语言呢。

那么,我们该怎么办?如果您不介意根据泛型和条件类型创建一些复杂的类型,则可以引导编译器沿着依次缩小每个属性的并集的方向进行。我的直觉是:拥有一个联合U ...并将其初始化为Events。然后查看传递到trackIt()中的候选值。首先检查name属性。如果U的任何成员匹配,请将U缩小为这些值。否则,将U保留下来。然后依次对typelocationsubType属性执行相同的操作。最后,在某种意义上,U将与候选值的类型“最接近”。然后,您要使编译器将候选值与U进行比较,并希望这些错误会更有针对性。

这是我的实现方式:

// Idx<T, K> looks up the K property of T, or returns never 
type Idx<T, K extends keyof any> = K extends keyof T ? T[K] : never;

// IfNever<T, V> evaluates to T unless T is never, in which case it is V
type IfNever<T, V> = [T] extends [never] ? V : T

// DiscriminateBy<U, K, T> takes a union type U, a key type K, and a candidate
// type T, and narrows U to all those whose values at the K property match that
// of T.  This might end up returning never, if there is no match.
type DiscriminateBy<U, K extends keyof any, T> =
    U extends any ? Idx<T, K> extends Idx<U, K> ? U : never : never

// Zoom<U, K, T> is similar to DisciminateBy<U, K, T>, but if there is no
// match it returns the full union U.
type Zoom<U, K extends keyof any, T>
    = IfNever<DiscriminateBy<U, K, T>, U>;

// Given E, EventsWithBetterErrors<E> uses Zoom to focus in on
// the Events union by comparing name, type, location, and subType properties,
// in that order.  
type EventsWithBettorErrors<E> =
    E extends Events ? E :
    Zoom<Zoom<Zoom<Zoom<Events, "name", E>, "type", E>,
        "location", E>, "subType", E>

// the call signature of trackIt is generic, where the e parameter will
// be inferred as having type E, and it is then compared to EventsWithBetterErrors<E>
async function trackIt<E>(e: EventsWithBettorErrors<E>): Promise<boolean>;
// keep the simpler implementation signature
async function trackIt({ name, ...payload }: Events) {
    return true;
}

我希望内联注释能解释这一点。

现在让我们看看它是如何工作的:

trackIt({
    name: "A",
    type: 1,
    location: "types",
    subType: "literal"
}) // okay

因此仍然会编译一个有效的调用。让我们尝试您的示例:

trackIt({
    name: "A",
    type: 2,
    location: "some",
    subType: "oops" // error!
    // Type '"oops"' is not assignable to type '"shared" | "not"'.
});
/* function trackIt<{
    name: "A";
    type: 2;
    location: "some";
    subType: "oops";
}>(e: A2): Promise<boolean> */

您会看到它推断e的类型应为A2(因为ZoomA1 | A2的位置是name,然后是{{ 1}}到Zoom之间再加上A2,然后随后的type范围不再缩小),因此错误集中在Zoom属性上。还有一个:

subType

在这里,trackIt({ name: "B", type: 2, // error! // Type '2' is not assignable to type '1'. location: "be", subType: "oops" // error! // Type '"oops"' is not assignable to type '"not" | "probably"'. }); /* function trackIt<{ name: "B"; type: 2; location: "be"; subType: "oops"; }>(e: B): Promise<boolean> */ 的{​​{1}}属性使编译器从namee停留在Zoom。因此,EventsB属性都被标记为不符合type


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

Link to code