Search code examples
typescript

Typescript map number type to union of string literals (numbers)


I'm trying to map a property of type number to a property of type string literals (by union).

Simplified example:

interface Destination {
  stringLiteralNumber: '0' | '1' | '2';
}

// Assume pre-validated to be between 0 - 2
const actualNumber: number = 1;

const mapped: Destination = {
  // How to map so Typescript is happy?
  stringLiteralNumber: actualNumber,
}

Solution

  • It's not that easy in the case where the actualNumber type is number. If you had the type narrowed to the literal union of each allowed number (i.e. 0 | 1 | 2), then you could use string interpolation (e.g. ${actualNumber}) to get the correct type. This is different than casting it using the String constructor, like shown below...

    type MyNumber = 0 | 1 | 2;
    type MyNumberString = '0' | '1' | '2';
    interface Destination {
      stringLiteralNumber: MyNumberString;
    }
    
    const actualNumber: MyNumber = 1; // typed as 0 | 1 | 2
    
    const mapped: Destination = {
      stringLiteralNumber: String(actualNumber),
    //^^^^^^^^^^^^^^^^^^^ Error: Type 'string' is not assignable to type 'MyNumberString'.   
    }
    

    TS Playground link

    So if you haven't narrowed the type, you could use a type predicate to manually narrow the type. Although, this would require duplicating the validation, then using a type guard.

    type MyNumber = 0 | 1 | 2;
    type MyNumberString = '0' | '1' | '2';
    interface Destination {
      stringLiteralNumber: MyNumberString;
    }
    
    const actualNumber: MyNumber = 1;
    
    function isMyNumber(n: number): n is MyNumber {
      return [0, 1, 2].includes(n); // return boolean to manually validate type
    }
    
    if (isMyNumber(actualNumber)) {
      // inside this block `actualNumber` is `MyNumber` type
      const mapped: Destination = {
        stringLiteralNumber: `${actualNumber}`,
      }
    }
    

    TS Playground link

    Type predicates can be dangerous because they can be wrong.

    function everythingIsString(value: unknown): value is string {
      return true; // always true no matter the input
    }
    

    But this approach is better than using as.

    Really you should try to set this value to the correct type where you are doing the validation, then pass it wherever you need. That could look something like this...

    type MyNumber = 0 | 1 | 2;
    type MyNumberString = '0' | '1' | '2';
    interface Destination {
      stringLiteralNumber: MyNumberString;
    }
    
    function isMyNumber(n: number): n is MyNumber {
      return [0, 1, 2].includes(n);
    }
    
    const someNumber: number = 2
    
    // `someNumber` is of type `number`
    if (!isMyNumber(someNumber)) {
      throw new Error('Bad validation');
    }
    
    // `someNumber` is of type `MyNumber`
    const someNumberString: MyNumberString = `${someNumber}` // No error 👍
    

    TS Playground link

    PS - Im assuming you posted your question correctly as '0' | '1' | '2', but it it was meant to be 0 | 1 | 2 that would change things