I commonly use type aliases to restrict possible string values :
type MyType = 'string1' | 'string2' | 'string3';
This is handful in switch statements to do specific job depending on this value.
However, is it possible to have a type that is Not of this strings ?
Here's a live sample of what I'm trying to achieve.
Basically, I get data from an api. The data contains several mixed items with a type
attribute that define what kind of data the item consists in.
// Get data from an API
const data: Contact[] = [
{
type: 'customer', accountId: 42, email: '[email protected]'
},
{
type: 'supplier', deliveryArea: 'Europe', email: '[email protected]'
},
{
type: 'AnotherTypeOfContact', email: '[email protected]'
}
];
Which I map to
type ContactBase = {
email: string;
}
type Customer = ContactBase & {
type: 'customer';
accountId: number;
}
type Supplier = ContactBase & {
type: 'supplier';
deliveryArea: 'Europe'
}
type BasicContact = ContactBase & {
type: string; // Should be any other value than the one set before
}
type Contact = Customer | Supplier | BasicContact;
I want to iterate over the data and apply a specific behavior for certain types (but not all), and fallback to a simple behavior for others.
However, this does not compiles.
Here's what I tried:
// Loop over data, do something specific for well known types and fallback for others
for (let i = 0; i < data.length; i ) {
const item = data[i];
switch (item.type) {
case 'supplier':
console.log(`${item.email} is a supplier which ships in ${item.deliveryArea}`);
break;
case 'customer':
console.log(`${item.email} is a customer with account id ${item.accountId}`);
break;
default:
console.log(`${item.email} is a contact of type ${item.type}`)
break;
}
}
As soon as every well known type has a dedicated case
statement, it stops to compiles.
If I remove the type
from the BasicContact
type, it does not compiles.
I also tried to exclude string using type: Exclude<string, 'customer' | 'supplier'>
, but it still does not compile.
How to fix ?
CodePudding user response:
You can't exclude specific string literals from the string
type. (It would be cool, but you can't [edit: at least, not yet, thanks as always jcalz!]. :-) )
Instead, you could define your Contact
type like this:
type SpecificContact = Customer | Supplier;
type Contact = SpecificContact | BasicContact;
Then have a type predicate that tells you whether a contact is a specific or general type:
function isSpecificContact(contact: Contact): contact is SpecificContact {
return contact.type === "supplier" || contact.type === "customer";
}
Then the loop can branch depending on whether it's a specific type of contact:
for (let i = 0; i < data.length; i ) {
const item = data[i];
if (isSpecificContact(item)) {
switch (item.type) {
case "supplier":
console.log(`${item.email} is a supplier which ships in ${item.deliveryArea}`);
break;
case "customer":
console.log(`${item.email} is a customer with account id ${item.accountId}`);
break;
}
} else {
console.log(`${item.email} is a contact of type ${item.type}`);
}
}
I don't like repeating strings as I have to in the type predicate above. To avoid that, you could have this constant object:
const ContactTypes = {
customer: "customer",
supplier: "supplier",
} as const;
Then the types look like this:
type Customer = ContactBase & {
type: typeof ContactTypes.customer,
accountId: number;
};
and the type predicate doesn't repeat strings:
function isSpecificContact(contact: Contact): contact is SpecificContact {
return contact.type in ContactTypes;
}
You can even use them in the loop (see the ***
lines), though repeating ourselves there is less of an issue because that'll be checked by the compiler, so it may be a bit overboard:
for (let i = 0; i < data.length; i ) {
const item = data[i];
if (isSpecificContact(item)) {
switch (item.type) {
case ContactTypes.supplier: // ***
console.log(`${item.email} is a supplier which ships in ${item.deliveryArea}`);
break;
case ContactTypes.customer: // ***
console.log(`${item.email} is a customer with account id ${item.accountId}`);
break;
}
} else {
console.log(`${item.email} is a contact of type ${item.type}`);
}
}