netdjw
netdjw

Reputation: 6007

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?

Upvotes: 1

Views: 556

Answers (2)

ford04
ford04

Reputation: 74710

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(";")

Upvotes: 3

ThomasThiebaud
ThomasThiebaud

Reputation: 11999

I'm not sure how you want to use CSVSeparator, but you may be able to use an enum instead

enum CSVSeparator {
  Semicolon = ";",
  Comma = ",",
  Space = " ",
  Pipe = "|",
  Colon = ".",
  Tabulator = "\t"
}

Upvotes: 3

Related Questions