looking for some help in typing a factory function that accepts a single enum as a paramter and returns a mapper function.
// enumeration of possible partners
enum Partner {
Google = 'google',
Microsoft = 'microsoft'
}
// lets say it's a domain entity, we'll map partner DTOs to it
type Entity = {
id: string
}
// DTO and mapper function for Partner.Google
type GoogleDto = {
google_id: string
}
function mapFromGoogle(dto: GoogleDto): Entity {
return {
id: dto.google_id
}
}
// DTO and mapper function for Partner.Microsoft
type MicrosoftDto = {
ms_id: number
}
function mapFromMicrosoft(dto: MicrosoftDto): Entity {
return {
id: String(dto.ms_id)
}
}
// And here I am trying to use a conditional type
// to check which enum value is passed as an argument
// and to provide a correct return type
function toEntity<T extends Partner>(partner: T): T extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft {
switch(partner) {
case Partner.Google:
return mapFromGoogle // Type '(dto: GoogleDto) => Entity' is not assignable to type 'T extends Partner.Google ? (dto: GoogleDto) => Entity : (dto: MicrosoftDto) => Entity'
case Partner.Microsoft:
return mapFromMicrosoft // Type '(dto: MicrosoftDto) => Entity' is not assignable to type 'T extends Partner.Google ? (dto: GoogleDto) => Entity : (dto: MicrosoftDto) => Entity'
default:
throw new Error('Unsupported partner')
}
}
const e1 = toEntity(Partner.Google)({ google_id: 'id' })
const e2 = toEntity(Partner.Google)({ google_id: 'id', ms_id: 4 }) // 'ms_id' does not exist in type 'GoogleDto'
const e3 = toEntity(Partner.Microsoft)({ ms_id: 10 })
const e4 = toEntity(Partner.Microsoft)({ ms_id: 10, google_id: 'asd' }) // 'google_id' does not exist in type 'MicrosoftDto'
const e5 = toEntity(Partner.Google)({}) // Property 'google_id' is missing in type '{}' but required in type 'GoogleDto'
const e6 = toEntity(Partner.Microsoft)({}) // Property 'ms_id' is missing in type '{}' but required in type 'MicrosoftDto'
The resulting function works as expected, it correctly relies on a provided partner and errors if invalid DTO id provided.
But TS shows error when I'm trying to return a specific mapper function from the toEntity
function inside the case
block.
Asking if someone can point me to the right direction in order to solve this case.
I've tried converting enumeration to union, but it also doesn't work.
Also tried to remove return type relying on TS to infer the type of returned mapper function, but in this case type checking doesn't work when toEntity
is called.
The TypeScript type checker is not really able to reason about what values might or might not be assignable to a conditional type that depends on an as-yet unspecified generic type parameter. It defers evaluation of such types. Inside the body of
toEntity()
, the typeis such a generic conditional type, and so the compiler is not sure what it's going to be... it doesn't know exactly what
T
is, so it knows almost nothing aboutT extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft
.You might think that your
switch
/case
statements that check thepartner
parameter against the different possibilities would help narrow/re-constrainT
, but this just doesn't happen, at least as of TypeScript 4.9.The canonical feature request for something better is microsoft/TypeScript#33912. It's been open for quite a while and there's no indication when it might be implemented, if ever.
For now, if you want to proceed, you'll need to work around it.
The best workaround in my opinion is to refactor your operations so that they are represented as a generic property lookup instead of a generic
switch
/case
. The compiler understands generic indexed access types in a way it does not understand generic conditional types.Here's one way to write that:
The
partnerMap
object encodes the desired input-output relationship oftoEntity()
as key-value pairs. ThetoEntity
call signature says that the return type is related to the input type via a lookup into the type ofpartnerMap
. And the implementation has been changed to match. This compiles because the type checker agrees that looking up a key of typeK
in a value of typePartnerMap
will yield a value of typePartnerMap[K]
.And let's just make sure that it works as desired from the caller's side:
Looks good; this part hasn't changed from your version, so all the test cases behave the same.
Playground link to code