3

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.

3 Answers 3

12

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

Sign up to request clarification or add additional context in comments.

2 Comments

I don't know if OP wants a readonly tuple, as their original post annotates FISH as a readonly array. In any case, it's an easy fix; annotate the return type of makeSubAnimals with Readonly<T> and remove the as const.
The tuple readonly ['a', 'b'] is assignable to readonly ('a' | 'b')[], so I'm not sure it makes difference. The only difference is if you are indexing it with a numeric literal, which is assignable to the union of possible values, which also seems fine.
2

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.

1 Comment

Well, same as my comment on Alex's answer, we don't know if OP wants a readonly tuple instead of a readonly array.
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.