带有动态 case 字符串的 switch-case 块的类型推断

Type-inference for switch-case block with dynamic case strings

棘手的 TS 挑战。

假设一个对象有两个对象 type 属性 和特定的 属性、fooDatafoo2Data,特定于 type 对象的值。

例如:

const dynamicString: string = 'abc'

const fooTypeName = `foo${dynamicString}` as const // const fooTypeName: `foo${string}`
const foo2TypeName = `foo${dynamicString}2` as const // const foo2TypeName: `foo${string}2`

type FooObj = {
  type: typeof fooTypeName
  fooData: string
}

type Foo2Obj = {
  type: typeof foo2TypeName
  foo2Data: string
}

假设将两种对象类型联合起来,创建一个“所有可能的对象”类型,AllObjs:

type AllObjs = FooObj | Foo2Obj

现在,假设要创建一个函数来提取类型为 AllObjs 的对象的“数据”值。明智的做法是使用 switch-case 块,如下所示:

const extractData = (obj: AllObjs) => {
  switch (obj.type) {
    case fooTypeName:
      return obj.fooData
    case foo2TypeName:
      return obj.foo2Data
    default:
      return null
  }
}

当前有错误。由于 fooTypeNamefoo2TypeName 的特定选择值,Typescript 无法在开关中进行类型推断。如果将 2 以外的任何字符附加到 fooTypeName,或在其前面加上任何字符,它就会起作用。打字稿在这里做什么,为什么?

我认为这与集合论有关,以及 foo2TypeName 如何可能 包含 fooTypeName。然而,仔细检查代码表明这是不可能的,因为对 dynamicString 的共享依赖(即不可能以 fooTypeName 和 [=20 的方式定义 dynamicString =] 是一样的)。

Typescript就这么受限了吗?

编辑

正如@jcalz 和@catgirlkelly 所解释的,即使 fooTypeNamefoo2TypeName 不可能相同(无论 dynamicString 的值是多少),因为每个引用dynamicString 被视为任何东西,foo${string}2 包含 foo${string},这是一个编译器限制,它不会将两个字符串视为不同的,因此防止 switch-case 块中的类型推断。

Playground link

const dynamicString: string = 'abc'

// const fooTypeName = `foo${dynamicString}1` as const
const fooTypeName = `foo${dynamicString}` as const
const foo2TypeName = `foo${dynamicString}2` as const

fooTypeName的类型是

`foo${string}`

foo2TypeName的类型是

`foo${string}2`

fooTypeNamefoo2Typename 宽,因此 TypeScript 将两者的并集简化为 foo${string},不允许您区分 AllObjs 的两个组成部分。

但为什么它更宽?

考虑以下情况:

fooTypeName = "fooAnythingGoes2" // ✅ foo, followed by any string
foo2TypeName = "fooAnythingGoes2" // ✅ foo, followed by any string and then 2

显然,这两个分配都是有效的,而且它们是相同的。现在,如果我们在 fooTypeName 的末尾添加任何其他字符:

const fooTypeName = `foo${dynamicString}.` as const // foo${string}.

它们不同:

fooTypeName = "fooAnythingGoes2" //  foo, followed by any string and then .
fooTypeName = "fooAnythingGoes." // ✅ foo, followed by any string and then .
foo2TypeName = "fooAnythingGoes2" // ✅ foo, followed by any string and then 2

列出了更多可能有助于您理解的案例here