Home > Blockchain >  Type for const string array containing subset of string literal union type
Type for const string array containing subset of string literal union type

Time:10-25

I have a string literal union type Animal:

type Animal = 'GOAT' | 'GIRAFFE' | 'SALMON' | 'TUNA'

I also have a type Fish which is a subset of Animal:

type Fish = Extract<Animal, 'SALMON' | 'TUNA'> // type Fish = "SALMON" | "TUNA"

Now I want to have an array of string containing the Fish, so that I can use that array in the logic (e.g. to use the includes() function). I could just define the array separately:

const FISH: ReadonlyArray<Fish> = ['SALMON', 'TUNA']

but now I have to maintain the list of fish in two places.

What I want to know is: is it possible to use the values in the array to define which string literals to extract for the Fish type while also only allowing the array to contain Animal strings?

Something like:

const FISH: ReadonlyArray<SubsetOfStringLiterals<Animal>> = ['SALMON', 'TUNA'] // Would error if it contained a value not in Animal
type Fish = typeof FISH[number] // type Fish = "SALMON" | "TUNA"

where SubsetOfStringLiterals would essentially be a utility like Partial but for string literal union types.

CodePudding user response:

If you want to apply a constraint to a type and also infer something more specific, then you need to either use a generic function or the new satisfies operator.


Coming in the next Typescript version (4.9, currently in beta) is the satisfies operator which works nicely here.

// fine
const FISH = ['SALMON', 'TUNA'] as const satisfies ReadonlyArray<Animal>

// error
const FISH_BAD = ['COFFEE'] as const satisfies ReadonlyArray<Animal>

See playground


Typescript 4.8 or below, you'll need to use a simple generic identity function.

function makeSubAnimals<T extends readonly Animal[]>(arr: T) { return arr }

const FISH = makeSubAnimals(['SALMON', 'TUNA'] as const) // fine
const FISH_BAD = makeSubAnimals(['COFFEE'] as const) // error

See Playground

CodePudding user response:

An alternative to Alex Wayne's solution ( 1) would be to make the compiler infer the type of FISH, and then separately ensure that Fish is really a subtype of Animal.

For example:

type Animal = 'GOAT' | 'GIRAFFE' | 'SALMON' | 'TUNA'

const FISH = ['SALMON', 'TUNA'] as const;

type Fish = typeof FISH[number] // type Fish = "SALMON" | "TUNA"

// dead code, but fails to compile if you add a 'TROUT' to FISH
((f: Fish) : Animal => f)

Playground.

  • Related