Home > Software design >  Type-inference for switch-case block with dynamic case strings
Type-inference for switch-case block with dynamic case strings

Time:05-23

A tricky TS challenge.

Say one has two objects with a type property, and a specific property, fooData or foo2Data, that is specific to the type value of the object.

For example:

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
}

Say one unionizes the two object types, to create an "all possible objects" type, AllObjs:

type AllObjs = FooObj | Foo2Obj

Now, say one wants to create a function that extracts the "data" value of an object typed as AllObjs. The sensible approach would be a switch-case block, as follows:

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

This currently errors. Typescript is not able to do type inference in the switch due to the specific chosen values for fooTypeName and foo2TypeName. If one appends any character other than 2 to fooTypeName, or prepends it with any character, it works. What is typescript doing here, and why?

I assume that it is something to do with set theory, and how foo2TypeName could potentially contain fooTypeName. However careful inspection of the code shows this to be impossible, because of the shared dependence on dynamicString (i.e. it is impossible to define dynamicString in such a way such that fooTypeName and foo2TypeName are the same).

Is Typescript just limited in this way?

Edit

As @jcalz and @catgirlkelly explain, even though it is impossible for fooTypeName and foo2TypeName to be the same (no matter what value of dynamicString), because each reference to dynamicString is treated as anything, foo${string}2 contains foo${string}, it is a compiler limitation that it does not treat the two strings as different, hence preventing type inference in the switch-case block.

Playground link

CodePudding user response:

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's type is

`foo${string}`

foo2TypeName's type is

`foo${string}2`

fooTypeName is wider than foo2Typename, so TypeScript simplifies the union of the two to just foo${string}, disallowing you to discriminate the two constituents of AllObjs.

But why is it wider?

Consider the following case:

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

Clearly, both assignments are valid, and they are the same. Now if we add any other character to the end of fooTypeName:

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

They're different:

fooTypeName = "fooAnythingGoes2" //            
  • Related