NJRBailey
NJRBailey

Reputation: 89

Type for const string array containing subset of string literal union type

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.

Upvotes: 3

Views: 1826

Answers (3)

snnsnn
snnsnn

Reputation: 13610

No need for type casting:

type Animal = 'GOAT' | 'GIRAFFE' | 'SALMON' | 'TUNA'
type Fish = Extract<Animal, 'SALMON' | 'TUNA'>
type ArrayOfFish = Array<Partial<Fish>>;

let arr: ArrayOfFish = ['SALMON']; // Accepts SALMON | TUNA

Upvotes: 0

Alex Wayne
Alex Wayne

Reputation: 187004

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

Upvotes: 12

ij7
ij7

Reputation: 369

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.

Upvotes: 2

Related Questions