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.
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" //