Arash Motamedi
Arash Motamedi

Reputation: 10672

TypeScript union of string and string literals

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 

Upvotes: 32

Views: 12466

Answers (2)

Maciej Kravchyk
Maciej Kravchyk

Reputation: 16607

Hinted string

Below is a type based on @jcalz answer to provide hinted string functionality.

/**
 * Constructs a string type with type hints for known values.
 */
// eslint-disable-next-line @typescript-eslint/ban-types
type HintedString<KnownValues extends string> = (string & {}) | KnownValues;

let nodeId: HintedString<':root' | ':trash'> = ':root';

nodeId = '454v3tv3';

Playground

However, do note that such "hinted string" is not type safe and is not recommended to use in public definitions that will be used throughout the codebase. I.e. in the above example, if I wanted to rename root to something else, I would not get any hints from TypeScript.

Type-safe alternatives

Object wrapper

The most universal one would be to wrap the string type in an object or an array, which while it may not look pretty, is type-safe.

I.e.

type Node = ':root' | ':trash' | [string]
// or
type Node = ':root' | ':trash' | { id: string }

Template literal type

In certain cases a much nicer solution is available - template literals.

type NodeId = `node_${string}`
type Target = ':root' | ':trash' | NodeId

let target: Target;

target = ''; // error
target = 'node_3r3rfewfwf'; // ok
target = ':root'; // ok

Playground

The additional benefit in this case is that NodeId type is more meaningful than just an alias to string and enforces extra type checking. Of course, it makes sense to apply it only in certain situations.

Upvotes: 11

jcalz
jcalz

Reputation: 327774

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

Upvotes: 64

Related Questions