Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript union of string and string literals

Tags:

typescript

I'd like to create a type definition for one of known string literals, or any string, e.g:

type IColor = "blue" | "green" | "red" | string;

TS doesn't complain about that type definition, but it also doesn't help with intellisense. My goal would be to define a function that accepts one of known colors or any color string.

const knownColors = {
    green: "#66bb6a",
    red: "#ef9a9a",
    blue: "#81d4fa",
} as const;

function getColor(color: keyof typeof knownColors | string): string {
   if (color in knownColors) 
      return knownColors[color as keyof typeof knownColors];

   return color;
}

Usage:

getColor("blue");    // "blue" is a known color, returns "#81d4fa" 
getColor("black");   // "black" is not a known color, returns "black"

I want intellisense to be intelligent

getColor("_          // as I type this line, 
          ^ blue     // I want these suggestions
          ^ green
          ^ red 
like image 942
Arash Motamedi Avatar asked Apr 05 '20 18:04

Arash Motamedi


1 Answers

This is currently considered a design limitation (see microsoft/TypeScript#29729) and/or a missing feature (see microsoft/TypeScript#33471) of TypeScript. From the type system's point of view, string is a supertype of any string literal like "blue" or "red", and so a union string | "blue" | "red" is the same as string and the compiler aggressively reduces such unions to string. This is completely correct as far as type safety goes. But it's not great from the point of view of documentation or IntelliSense, as you've seen.

Luckily the linked TypeScript issues suggest some workarounds which you might find useful. One is to represent the union type in a way that the compiler does not aggressively reduce. The type string & {} is conceptually the same as string, since the empty type {} matches any non-null and non-undefined type. But the compiler does not perform this reduction (at least as of TS 3.8). From this type you can build your union like (string & {}) | "red" | "green" | "blue", and the compiler will keep this representation long enough to give you IntelliSense hints:

function getColor(color: (keyof typeof knownColors) | (string & {})): string {
    if (color in knownColors)
        return knownColors[color as keyof typeof knownColors];
    return color;
}

This accepts and rejects the same inputs as before:

getColor("red"); // okay
getColor("mauve"); // okay
getColor(123); // error

But you can verify that IntelliSense produces the following:

IntelliSense suggesting "red","green","blue"


The type signature might be a little more confusing than you'd like. You could also get a similar effect by using overloads instead:

function getColorOvld(color: (keyof typeof knownColors)): string;
function getColorOvld(color: string): string;
function getColorOvld(color: string): string {
    if (color in knownColors)
        return knownColors[color as keyof typeof knownColors];
    return color;
}

getColorOvld("red"); // okay
getColorOvld("mauve"); // okay
getColorOvld(123); // error

This also gives you reasonable IntelliSense:

enter image description here


Okay, hope that helps!

Playground link to code

like image 96
jcalz Avatar answered Sep 28 '22 20:09

jcalz