网站搜索

Typescript 元组成员的类型解析 - 为什么这能编译?


操场

你可以在这里找到带有代码的游乐场

给定以下类型定义:

type State = ["IDLE"] | ["STARTING_NEW_CONVERSATION"] | ["CHANNEL_ACTIVE", "SYNCED" | "EXPLORING"];
const state: State = ...

以及以下辅助方法:

export function matches<T extends [string] | [string, string], X = T[0], Y = T[1]>(
  value: T,
  check: T[0],
  check2?: Extract<T, [X, Y]>[1]
) {
  return value[0] === check && (check2 ? value[1] === check2 : true);
}

// another variation...

export function matches<T extends [string] | [string, string]>(
  value: T,
  check: T[0],
  check2?: T extends [infer X, infer Y] ? Extract<T, [X, Y]>[1] : never
) {
  return value[0] === check && (check2 ? value[1] === check2 : true);
}

它大多数情况下有效——不过,我可以这么称呼它

matches(state, "IDLE", "EXPLORING")

对于编译器来说这是可以的——我本来预期第二个参数的类型会被解析为 never 或 undefined。

  • 值 ["IDLE", "EXPLORING"] 不能赋给 state
  • 正确编写 Extract<State, ["IDLE", "EXPLORING"]> 会提取 never

所以,我的问题是

为什么对“matches”的调用使用表示无效类型的值会成功?

答案#1

TypeScript 不会根据你传入第二个参数的值来缩小 T 的类型。

给出这段代码:

type State = ["IDLE"] | ["STARTING_NEW_CONVERSATION"] | ["CHANNEL_ACTIVE", "SYNCED" | "EXPLORING"];
const state: State = {} as any

export function matches<T extends [string] | [string, string], X = T[0], Y = T[1]>(
  value: T,
  check: T[0],
  check2?: Extract<T, [X, Y]>[1]
) {
  return value[0] === check && (check2 ? value[1] === check2 : true);
}

matches(state, "IDLE", "EXPLORING")

TypeScript 推断以下参数类型:

value: State
check: "IDLE" | "STARTING_NEW_CONVERSATION" | "CHANNEL_ACTIVE"
check2: "SYNCED" | "EXPLORING"

当你将 "IDLE" 作为第二个参数传入时,TS 只是看了一眼,然后说“是的,这匹配 "IDLE" | "STARTING_NEW_CONVERSATION" | "CHANNEL_ACTIVE"”,然后继续。它不会收窄任何内容。

如果你需要根据第二个和第三个参数之间的关系来强制类型约束,那么你应该在模板参数列表中放置一个你要约束的公共类型,并从两个参数中引用它:

function matches<T extends State, X extends T[0], Y extends T[1]>(
  value: T,
  check: X,
  check2?: Extract<T, [X, Y]>[1],
) {
  return value[0] === check && (check2 ? value[1] === check2 : true);
}

type State = ["IDLE"] | ["STARTING_NEW_CONVERSATION"] | ["CHANNEL_ACTIVE", "SYNCED" | "EXPLORING"];
const state: State = {} as any

matches(state, 'IDLE', ) // WORKS
matches(state, 'CHANNEL_ACTIVE', 'EXPLORING') // Works
matches(state, 'IDLE', 'EXPLORING') // ERROR: Argument of type '"EXPLORING"' is not assignable to parameter of type 'undefined'.

关键区别在于,我们在模板列表中定义类型X,然后在两个参数约束中使用它。这迫使TS推断出一个满足两个参数的单一类型X。如果没有这样做,你的参数约束彼此之间就没有关联。

(编辑:更新为一个更简化的解决方案,更接近你原来的代码)