Lesson 44 of 80
as const doesn't just lock a literal's type — on an array it swaps a mutable, order-losing array type for an exact, position-by-position readonly tuple.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The type inference lesson already covers what as const does to primitives (locking "dark" instead of widening to string) and how to derive a union from a const array. This lesson is about the other half: what as const does to arrays specifically — it doesn't just keep the values readonly, it changes the shape of the type itself, from a general array to an exact tuple.
const pair1 = [1, "loading"];
// inferred: (string | number)[] — an array of "either", length unknown, order lost
const pair2 = [1, "loading"] as const;
// inferred: readonly [1, "loading"] — exactly two elements, exact types, exact positions
Without as const, TypeScript sees a mixed-content array and merges every element into one union type, throwing away which value was at which position. With as const, it keeps each position's exact type — that's a tuple, not an array, and it's the difference between "some number or string, who knows how many" and "a number, then a string, always."
function useToggle(initial: boolean) {
let value = initial;
const toggle = () => { value = !value; };
return [value, toggle];
// inferred: (boolean | (() => void))[] — order and identity both lost
}
const [isOpen, toggleOpen] = useToggle(false);
// isOpen: boolean | (() => void) 😬
// toggleOpen: boolean | (() => void) 😬
Destructuring that return value is nearly useless — TypeScript can't tell you that position 0 is the boolean and position 1 is the function, because the return type is just "an array containing either kind of thing." Adding as const to the return fixes it completely:
function useToggle(initial: boolean) {
let value = initial;
const toggle = () => { value = !value; };
return [value, toggle] as const;
// inferred: readonly [boolean, () => void]
}
const [isOpen, toggleOpen] = useToggle(false);
// isOpen: boolean — toggleOpen: () => void — exactly right, per position
This is exactly the problem React's own type declarations solve for the real useState — a hand-rolled hook with the same [value, setter] shape needs as const to get the same positional precision.
as const collapses every element into one union type and forgets the order — this is invisible until you destructure and every position gets the same overly-wide type instead of its own specific one.readonly in TypeScript — pair2.push(3) fails to compile, but nothing about the underlying JavaScript array is actually frozen at runtime; Object.freeze is the runtime equivalent, and the two are unrelated mechanisms that happen to overlap in intent.const copy = [...pair2] infers (string | number)[], not readonly [1, "loading"] — the tuple precision doesn't automatically survive a spread, since spreading is itself a new array literal being inferred fresh.as const only locks down literal expressions you write it on directly. const x = someVariable; const y = [x] as const doesn't retroactively narrow whatever type someVariable already had — it can only work with the information present at the point you apply it."Why would [value, setter] as const matter for a custom hook?" is a compact way to check whether someone understands that TypeScript infers array literals as unions by default, and that tuple precision — the thing that makes const [a, b] = fn() give a and b two genuinely different, correct types — has to be asked for explicitly.