Understanding “As Const” in TypeScript

Omari - Feb 1 - - Dev Community

TypeScript 3.4 introduced a new construct called “const assertions”. They tell TypeScript that your variable isn’t going to change, i.e., immutable, and that it should provide your type with as strict a type as possible. They affect different types in different ways, so in this article we’ll talk through how to use const assertions, and why they’re useful.

Strings/Numbers

Adding “as const” to a string/number will narrow the type to a specific value:

let foo = 'foo'; 
// let foo: string 
let foo = 'foo' as const; 
// let foo: 'foo't
Enter fullscreen mode Exit fullscreen mode

Or for numbers:

let foo = 7; // let foo: number 
let foo = 7 as const; // let foo: 7;
Enter fullscreen mode Exit fullscreen mode

It’s less useful for strings/numbers, since usually you could just define your variable using “const” for the same effect:

let foo = 7; // let foo: number 
const foo = 7; // const foo: 7;
Enter fullscreen mode Exit fullscreen mode

With the added benefit of runtime safety.

Sometimes you might not want to define a value as a variable though and you might just want to use a string literal, e.g. for returning a value. Then, “as const” does come in use. Take a look at this example:

type Colour = 'red' | 'green' | 'blue';
type Variant = 'light' | 'dark';
function createColourVariant(colour: Colour, variant: Variant) {
    return `${variant}-${colour}`;
} //function createColourVariant(colour: Colour, variant: Variant): stringty
Enter fullscreen mode Exit fullscreen mode

A simple function to create a colour variant name, imagine defining a palette for an app or something similar. The return type we get from this function is just a string.

You could create an explicit type for these colour variants:

type ColourVariant = `${Variant}-${Colour}`; //"light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue" function createColourVariant(colour: Colour, variant: Variant): ColourVariant { return `${variant}-${colour}`; }
Enter fullscreen mode Exit fullscreen mode

That might work fine for your use case, but we can also skip creating the type altogether with a const assertion:

function createColourVariant(colour: Colour, variant: Variant) {
    return `${variant}-${colour}` as const;
} //function createColourVariant(colour: Colour, variant: Variant): "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"
Enter fullscreen mode Exit fullscreen mode

Which is better? The first option is useful if you plan to reuse your type a lot, and also if you want the function to adhere to the type, rather than the type depending on that variable. The second option is shorter, and lets you skip the extra work of defining an extra type.

Something important to note is that you can only use “as const” with literal values:

const foo = 'foo' as const;
const bar = foo as const; //A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.
Enter fullscreen mode Exit fullscreen mode

Objects

Objects and arrays are where “as const” gets more interesting. For objects, “as const” changes all of the properties to readonly and narrows the values:

const myObject = { foo: 'bar', baz: 7 };
/* const myObject: { foo: string; baz: number; } */ 
const myObject = {
    foo: 'bar',
    baz: 7,
} as const; /* const myObject: { readonly foo: "bar"; readonly baz: 7; } */
Enter fullscreen mode Exit fullscreen mode

This means you won’t be able to change the values of the properties:

const myObject = { foo: 'bar', baz: 7 } as const;
myObject.baz = 6; //Cannot assign to 'baz' because it is a read-only property
Enter fullscreen mode Exit fullscreen mode

And you’ll lose access to any of the methods that mutate objects/arrays:

const me = { name: 'Omari', hobbies: ['coding', 'cooking', 'gaming'] } as const; //time to grow up :(
me.hobbies = ['coding', 'cooking']; //Cannot assign to 'hobbies' because it is a read-only property.
me.hobbies.pop(); //Property 'pop' does not exist on type 'readonly ["coding", "cooking", "gaming"]'. 
// yippee :)
Enter fullscreen mode Exit fullscreen mode

Arrays

“as const” turns arrays into tuples of readonly values:

const goodLanguages = ['typescript', 'csharp']; //const goodLanguages: string[] const goodLanguages = ['typescript', 'csharp'] as const; 
//const goodLanguages: readonly ["typescript", "csharp"]
Enter fullscreen mode Exit fullscreen mode

Not sure what a tuple is? Essentially it’s an ordered array of values. They’re useful for grouping variables together like an object, but a little less work.

So it’s especially useful for creating and handling tuples, because “as const” preserves the order and number of items in the array. Think of creating a custom React hook for toggling a value:

function useToggle(defaultValue = false) {
    const [active, setActive] = useState(false);
    const toggle = () => setActive((v) => !v);
    return [active, toggle];
}
Enter fullscreen mode Exit fullscreen mode

The inferred return type of our function is this:

(boolean | (() => void))[]
Enter fullscreen mode Exit fullscreen mode

We know our return type is a tuple where the first is the value, and the second is a function to toggle the value, but TypeScript has assumed that the value and the toggle function could be in any place, and the array could be any number of them.

So using our hook doesn’t work as you’d expect:

const [dialogOpen, toggleDialogOpen] = useToggle(); 
//const dialogOpen: boolean | (() => void) 
//const toggleDialogOpen: boolean | (() => void)
Enter fullscreen mode Exit fullscreen mode

This is where the “as const” steps in:

function useToggle(defaultValue = false) {
    const [active, setActive] = useState(false);
    const toggle = () => setActive((v) => !v);
    return [active, toggle] as const;
} //function useToggle(defaultValue?: boolean): readonly [boolean, () => void]
Enter fullscreen mode Exit fullscreen mode

We can then use the tuple returned by our hook as expected:

const [dialogOpen, toggleDialogOpen] = useToggle(); 
//const dialogOpen: boolean 
//const toggleDialogOpen: () => void
Enter fullscreen mode Exit fullscreen mode

Const vs “As Const”

Despite how similar they sound, declaring a variable as a const variable is different to adding “as const” onto the end of a variable.

Declaring a const variable tells TypeScript that the reference your variable refers to will not change. Strings and numbers in JavaScript are immutable, so if you’re declaring a variable either way then for TypeScript, there is no difference between the two techniques. The difference lies in the fact that “const” is a JavaScript feature, whereas “as const” is a TypeScript feature.

const link = 'youtu.be/pHqC0uoatag'; 
//const link: 'youtu.be/pHqC0uoatag'; 

let link = 'youtu.be/pHqC0uoatag' as const; 
//let link: "youtu.be/pHqC0uoatag"
Enter fullscreen mode Exit fullscreen mode

Running the following code with Bun, for example. This will run:

let link = 'youtu.be/pHqC0uoatag' as const; 
//const link: 'youtu.be/pHqC0uoatag'; 
link = ''; 
//Type '""' is not assignable to type '"youtu.be/pHqC0uoatag"'.
Enter fullscreen mode Exit fullscreen mode

But this will not:

const link = 'youtu.be/pHqC0uoatag'; 
//const link: 'youtu.be/pHqC0uoatag'; 
link = ''; 
//Cannot assign to 'link' because it is a constant.
Enter fullscreen mode Exit fullscreen mode

For objects and arrays, the difference lies in references vs values. Declaring an object/array using const tells TypeScript and JavaScript that that variable will never refer to another object/array, it does not mean that the values won’t change.

const bestNumbers = [1, 12, 24]; //great numbers 
bestNumbers = [1, 7, 8]; //won't work, this is a different array 
bestNumbers = [1, 12, 24]; //won't work, this is a different array 
//Cannot assign to 'bestNumbers' because it is a constant. 
bestNumbers.pop(); //Works fine
Enter fullscreen mode Exit fullscreen mode

You can modify the values in an object/array without modifying the reference.

Compared to “as const”, where you can’t change the reference, or the values:

const bestNumbers = [1, 12, 24] as const; //great numbers 
bestNumbers = [1, 7, 8]; //won't work, this is a different array 
bestNumbers = [1, 12, 24]; //won't work, this is a different array 
//Cannot assign to 'bestNumbers' because it is a constant. 
bestNumbers.pop(); //Now this won't work either 
bestNumbers[2] = 7; //Or this
Enter fullscreen mode Exit fullscreen mode

Con(st)clusion

So to sum everything up, “as const” or const assertions are a great TypeScript feature for giving your variables narrower types. They’re not always necessary, but they’re a great feature to have in your toolbelt, especially for creating tuples, and returning values with narrower types form functions. Hopefully, after this article, you now know when and where to use them. Thanks for reading!

Originally published at https://www.omarileon.me.

. . . . . . .