Home > Mobile >  How to set a type to be a string but not allowing some specific values?
How to set a type to be a string but not allowing some specific values?

Time:07-23

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}`);
    }
}

Playground link


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;
}

Playground link

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}`);
    }
}

Playground link

  • Related