TypeScript:区分联合和元组中的值

TypeScript: Discriminate union from value in a tuple

是否可以根据元组中的第一个元素来区分联合类型属性?

例如

type Landing = {
  tokens: ['landing']
};

type Month = {
  tokens: ['month', string]
};

type Day = {
  tokens: ['day', string, string]
};

type Route =
  | Month
  | Day
  | Landing;

let route: Route = getRoute();

if(route.tokens[0] === 'day') {
  // resolve to Day type
}

更新: 如果没有直接的方法来做到这一点,我会很高兴有一个自定义的类型保护,但还没有能够让一个工作。为了澄清,我想要一个可以区分联合的类型保护,而不是对每个变体进行显式检查。

例如

if(typeGuard(route, 'day')) {
    // compiler knows this id Day type
}
else if(typeGuard(route, 'month')) {
    // compiler knows this is Month type
}

使用类型保护,它会是这样的:

type Landing = {
  tokens: ['landing']
};
const isLanding = (x: any): x is Landing => x[0] === 'landing'

type Month = {
  tokens: ['month', string]
};
const isMonth = (x: any): x is Month => x[0] === 'month'

type Day = {
  tokens: ['day', string, string]
};
const isDay = (x: any): x is Day => x[0] === 'day'

type Route =
  | Month
  | Day
  | Landing;

let route: Route = getRoute();

if(isDay (route)) {
  // resolve to Day type
}

您可以在 official documentation(搜索类型保护)

中找到有关类型保护的更多信息

希望它能回答您的问题 ;)

编辑 自最初的答案以来,打字稿在区分联合方面变得更好,因此这与打字稿 3.3 的预期一样有效:

if (route.tokens[0] === "day") {
    // resolve to Day type
    route.tokens[0] === 'day'
} else if (route.tokens[0] === "landing") {
    // resolve to Landing type
    route.tokens[0] === 'landing'
} else {
    // resolve to Month type
    route.tokens[0] === 'month'
}

原版

虽然为每个联合成员编写类型保护的解决方案是一个完全有效的解决方案,但如果联合有很多成员,或者如果您稍后向联合添加额外的成员,它确实会产生问题。

您可以使用 Extract 条件类型创建自定义类型保护来保护所有可能的类型:

type Landing = {
    tokens: ['landing']
};

type Month = {
    tokens: ['month', string]
};

type Day = {
    tokens: ['day', string, string]
};

type Route =
    | Month
    | Day
    | Landing;

declare let route: Route;

function isRoute<T extends Route['tokens'][0]>(r: Route, type: T): r is Extract<Route, { tokens: [T, ...any[]] }> {
    return route.tokens[0] === type;
}

if (isRoute(route, 'day')) {
    // resolve to Day type
    route.tokens[0] === 'day'
} else if (isRoute(route, 'landing')) {
    // resolve to Landing type
    route.tokens[0] === 'landing'
} else {
    // resolve to Month type
    route.tokens[0] === 'month'
}

Playground link