I have a mapping for a discriminated union from a type of the union to a function using a member of the union as a parameter:
export interface Truncate {
type: 'truncate'
maxLength: number
}
export interface Append {
type: 'append'
suffix: string
}
export type Applicable = Truncate | Append
const applicableMap: { [K in Applicable['type']]: (base: string, applicable: Extract<Applicable, { type: K }>) => string } = {
'truncate': (s, applicable) => {
return s.slice(0, applicable.maxLength)
},
'append': (s, applicable) => {
return s + applicable.suffix
}
}
So far so good; TypeScript correctly infers the types of the applicable
argument in each function and performs an exhaustive checking on the keys, so I don't accidentally forget to add an entry to the map if I add a new member.
Now, I have an apply
function that picks a function from the map and tries to call it:
function apply(s: string, applicable: Applicable): string {
return applicableMap[applicable.type](s, applicable)
}
However, the fails to compile due to the folllowing:
Argument of type 'Applicable' is not assignable to parameter of type 'never'.
The intersection 'Add & Append' was reduced to 'never' because property 'type' has conflicting types in some constituents.
Type 'Add' is not assignable to type 'never'.ts(2345)
This is quite surprising to me, as I would expect TypeScript to correctly infer that this will always be a valid call.
The only fix I found so far is to call it as applicableMap[applicable.type](s, applicable as any)
, however I find using any
to be always a hack.
Now the question is:
Is this just a current TypeScript limitation, or is there a way to adjust the types here whilst preserving the type inference and exhaustive checking?
Check on https://github.com/microsoft/TypeScript/issues/30581 which seems like an acknowledged limitation in the Typescript compiler, and has roughly the same shape as your problem I think.