2

I want to use a single array const fruits = ['banana', 'apple', 'orange'] as a normal array, and as a type.

I should be able to do this: const x: fruits // => only accepts 'banana', 'apple' or 'orange'

And also be able to do this: @IsIn(fruits)


I've tried to declare the array as <const>, such as:

const fruits = <const>['banana', 'apple', 'orange']
type Fruits = typeof fruits[number] // this evaluates to type: "banana" | "apple" | "orange"

But @IsIn(fruits) will return the following error:

Argument of type 'readonly ["banana", "apple", "orange"]' is not assignable to parameter of type 'any[]'.
  The type 'readonly ["banana", "apple", "orange"]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.ts(2345)

So I thought if I created two arrays, a normal one and a readonly one, it should work. So I tried this:

const fruits = ['banana', 'apple', 'orange']
const fruits_readonly: <const>[...fruits]
type Fruits = typeof fruits_readonly[number]

But now Fruits evalutes to type: string instead of type: "banana" | "apple" | "orange".

1 Answer 1

3

It's true that const assertions produce objects and arrays with readonly elements. If you want to get the string-literal and tuple type benefits that the const assertion buys you while also un-readonly-ing the result, you could write a helper function to do that. I'll call it mutable():

const mutable = <T>(t: T): { -readonly [K in keyof T]: T[K] } => t

const fruits = mutable(['banana', 'apple', 'orange'] as const);
// const fruits: ["banana", "apple", "orange"]

That will work one level deep. If you have nested object/array types you might want to make a DeepMutable type and deepMutable() helper function:

type DeepMutable<T> =
    T extends object ? { -readonly [K in keyof T]: DeepMutable<T[K]> } : T

const deepMutable = <T>(t: T) => t as DeepMutable<T>;

That works the same for the above case,

const alsoFruits = deepMutable(['banana', 'apple', 'orange'] as const);
// const alsoFruits: ["banana", "apple", "orange"]

but the distinction becomes important with nested objects:

const readonlyDeepFruits = {
    yellow: ["banana", "lemon"],
    red: ["cherry", "apple"],
    orange: ["orange", "mango"],
    green: ["lime", "watermelon"]
} as const;
/* const readonlyDeepFruits: {
    readonly yellow: readonly ["banana", "lemon"];
    readonly red: readonly ["cherry", "apple"];
    readonly orange: readonly ["orange", "mango"];
    readonly green: readonly ["lime", "watermelon"];
} */

const partiallyMutableDeepFruits = mutable(readonlyDeepFruits);
/* const partiallyMutableDeepFruits: {
    yellow: readonly ["banana", "lemon"];
    red: readonly ["cherry", "apple"];
    orange: readonly ["orange", "mango"];
    green: readonly ["lime", "watermelon"];
} */

const fullyMutableDeepFruits = deepMutable(readonlyDeepFruits);
/* const fullyMutableDeepFruits: {
    yellow: ["banana", "lemon"];
    red: ["cherry", "apple"];
    orange: ["orange", "mango"];
    green: ["lime", "watermelon"];
} */

Okay, hope that helps. Good luck!

Link to code

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

2 Comments

Thanks, this works :) But I still don't understand why typeof fruits_readonly[number] doesn't evaluate to type: "banana" | "apple" | "orange". Would you explain to me, please?
Oh, sorry... in const fruits = ['banana', 'apple', 'orange'], fruits is inferred as type string[], so the compiler has already forgotten the identity of the particular strings passed into it. So [...fruits] will also be string[]. If you want the compiler to remember "banana" | "apple" | "orange" you need to tell it not to throw away information while it still has it... that's what as const does (among other things).

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.