So, I find myself to work with discriminated unions quite a bit, and I often encounter this issue:
I have a function with a parameter which type is a discriminated union, and I want to do something if said parameter belongs to a subset of the union
In order to do that, I can use the discriminant to narrow the type, and all is good.
The problem arises when the union grows in size, and I find myself with long chains of if (x.type === 'a' || x.type === 'b' || x.type === 'c' || ...)
In Javascript, in such cases I would just if (['a','b','c'].includes(x.type)), but this approach doesn't cut it for TS
Here is an example:
type Certificate = {
type: 'NATIONAL_ID',
nationality: string
} | {
type: 'PASSPORT',
passportNumber: string
} | {
type : 'INSURANCE_CARD',
provider: string
} | {
type: 'BIRTH_CERTIFICATE',
date: string
}| {
type: 'DEATH_CERTIFICATE',
date: string
}| {
type: 'MARRIAGE_CERTIFICATE',
date: string
}
// Works fine, but requires me to enumerate the discriminant cases one by one
const goodPrintDate = (certificate: Certificate) => {
if (certificate.type === 'BIRTH_CERTIFICATE' || certificate.type === 'DEATH_CERTIFICATE' || certificate.type === 'MARRIAGE_CERTIFICATE') {
console.log(certificate.date)
return
}
console.log(`certificate of type ${certificate.type} has no date`)
}
// Doesn't work
const badPrintDate = (certificate: Certificate) => {
const certificateTypesWithDate = ['BIRTH_CERTIFICATE', 'DEATH_CERTIFICATE', 'MARRIAGE_CERTIFICATE']
if (certificateTypesWithDate.includes(certificate.type)) {
// Only gets here if certificate type is 'BIRTH_CERTIFICATE', 'DEATH_CERTIFICATE', or 'MARRIAGE_CERTIFICATE', but TS does not know :(
console.log(certificate.date) //Property 'date' does not exist on type '{ type: "NATIONAL_ID"; nationality: string; }'
return
}
console.log(`certificate of type ${certificate.type} has no date`)
}
So, is there a way for me to improve the syntax of goodPrintDate so that I don't need to explicitly enumerate every time? E.g. extracting that logic to a separate function (isCertificateWithDate / isCertificateTypeWithDate)
I've tried doing some type gymnastics, using sets instead of array, but nothing working
The closest solution I can think of for your problem is using
switchwith fall-through:It does list all relevant cases in a fairly readable way. If you want to explore other type-narrowing options, I recommend to read https://www.typescriptlang.org/docs/handbook/2/narrowing.html.
Whether
includesshould narrow down types was discussed before, e.g. in https://github.com/microsoft/TypeScript/issues/36275, but the TS team decided against it as the added complexity is not worth the added value. Feel free to check this out as well for a better understanding of the problem.