问题陈述:
当创建由具有不同字段的对象组成的并集类型时,其中某些类型包含多个区分键,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”。
答案 0 :(得分:1)
TypeScript并不能真正让您自定义错误。有open suggestion允许类似Invalid<T>
之类的东西在每当对某种类型求值时都会生成编译器错误,并输出类型T
作为消息。不幸的是,这还没有实现,可用的解决方法非常难看。
您在这里面临的一般问题似乎是编译器无法找出(从开发人员的角度来看)导致类型检查失败的最相关原因。如果您有一个应该但不能分配给像X | Y | Z
这样的联合类型的值,则编译器必须确定要投诉的成员中的哪一个。它使用某种启发式方法,但总是存在边际情况。我看到另一个open suggestion可以通过选择“最接近”的成员以某种方式进行改进,但是它并没有详细说明“最接近”的含义(因此问题需要提出建议),并且在任何情况下都不属于该提议的一部分。语言呢。
那么,我们该怎么办?如果您不介意根据泛型和条件类型创建一些复杂的类型,则可以引导编译器沿着依次缩小每个属性的并集的方向进行。我的直觉是:拥有一个联合U
...并将其初始化为Events
。然后查看传递到trackIt()
中的候选值。首先检查name
属性。如果U
的任何成员匹配,请将U
缩小为这些值。否则,将U
保留下来。然后依次对type
,location
和subType
属性执行相同的操作。最后,在某种意义上,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
(因为Zoom
到A1 | 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}}属性使编译器从name
到e
停留在Zoom
。因此,Events
和B
属性都被标记为不符合type
。
哇!好的,希望能有所帮助;祝你好运!