Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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

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 ?

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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: 'bill@corp.bzh'
  },
  {
    type: 'supplier', deliveryArea: 'Europe', email: 'support@corp.com'
  },
  {
    type: 'AnotherTypeOfContact', email: 'rene@lataupe.com'
  }
];

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 ?

>Solution :

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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading