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
Or for numbers:
let foo = 7; // let foo: number
let foo = 7 as const; // let foo: 7;
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;
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
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}`; }
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"
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.
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; } */
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
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 :)
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"]
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];
}
The inferred return type of our function is this:
(boolean | (() => void))[]
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)
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]
We can then use the tuple returned by our hook as expected:
const [dialogOpen, toggleDialogOpen] = useToggle();
//const dialogOpen: boolean
//const toggleDialogOpen: () => void
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"
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"'.
But this will not:
const link = 'youtu.be/pHqC0uoatag';
//const link: 'youtu.be/pHqC0uoatag';
link = '';
//Cannot assign to 'link' because it is a constant.
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
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
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.