Search code examples
typescripttype-definition

How to declare custom type correctly with predefined acceptable values in TypeScript?


I have this type declaration:

declare const csvSeparator: unique symbol;
export declare type CSVSeparator = ';' | ',' | ' ' | '|' | '.' | '\t'
  & { readonly [csvSeparator]: 'CSVSeparator' };

Then I try to define this structure:

const predefinedColumnSeparators: ColumnSeparatorInterface[] = [
  {name: 'Semicolon', character: ';'},
  {name: 'Comma', character: ','},
  {name: 'Space', character: ' '},
  {name: 'Pipe', character: '|'},
  {name: 'Colon', character: '.'},
  {name: 'Tabulator', character: '\t'},
];

But in here I get an error Type '"\t"' is not assignable to type 'CSVSeparator'.

How can I declare my custom type correctly? Or is there any better way to define acceptable values?


Solution

  • You have made '\t' in CSVSeparator a nominal string type (also called branded primitive). The error in the assignment occurs, because a regular '\t' string cannot be assigned to this nominal '\t' anymore. Also the nominal typing (the intersection part) is only applied to '\t':

    type CSVSeparator = ... | ... | ('\t'  & { readonly [csvSeparator]: 'CSVSeparator' })`
    // read like this:              ^                                                   ^
    

    I think, you have two options here: 1. use regular strings (the simple one) or 2. branded strings (stronger types).

    1. Simple string (Code)

    export type CSVSeparator = ';' | ',' | ' ' | '|' | '.' | '\t'
    
    const predefinedColumnSeparators: { name: string; character: CSVSeparator }[]  = [
      {name: 'Semicolon', character: ';'},
      ...
      {name: 'Tabulator', character: '\t'},
    ];
    

    2. Branded string (Code)

    declare const csvSeparator: unique symbol;
    
    // nominal string type
    type Branded<T extends string> = T & { readonly [csvSeparator]: 'CSVSeparator' }
    
    // helper function: create branded string by applying a type assertion to it
    function brand<T extends string>(s: T) {
        return s as Branded<T>
    }
    
    export type CSVSeparator = Branded<';'> | Branded<','> | Branded<' '> | Branded<'|'>
        | Branded<'.'> | Branded<'\t'>
    
    const predefinedColumnSeparators: { name: string; character: CSVSeparator }[] = [
        { name: 'Semicolon', character: brand(';') },
        { name: 'Semicolon', character: ';' }, // error, no regular string assignable 
        ...
        { name: 'Tabulator', character: brand('\t') }
    ];
    

    The branding is just a type declaration at compile-time, you can use it as regular string:

    const regularString: string = brand(";")