Enhancing Code Robustness and Type Safety
TypeScript is a versatile language that empowers developers to write code that is not only maintainable but also type-safe. One of its powerful features, const assertion, plays a pivotal role in achieving these goals.
Understanding Const Assertion
In TypeScript, a const assertion can only be applied to a string, number, boolean, array, or object literal. This ensures that the assigned value remains constant and immutable throughout its usage.
// ❌ Error! A 'const' assertion can only be applied to a to a string, number, boolean, array, or object literal.
let a = (Math.random() < 0.5 ? 0 : 1) as const;
let b = (60 * 60 * 1000) as const;
// ✅ Works!
let c = Math.random() < 0.5 ? (0 as const) : (1 as const); // 0 | 1
let d = 3_600_000 as const; //3600000
Leveraging Const Assertion for Enums
One of the most compelling applications of Const assertion is its use as a replacement for traditional Enums. By creating TypeScript immutable objects, we can centralize configurations while avoiding the introduction of magic-strings in our code.
Additionally, with const assertion we can automatically infers the types from these objects with an utility type, enhancing type safety.
Infer the types with ValueOf<>
The ValueOf type utility mimics the behavior of keyof but for values. This enables us to extract the values from a constant object, creating a union type.
/**
* @description
* like ***keyof*** but for values
*
* @example
* ```typescript
* export const COLORS = {
* BLACK: 'black',
* WHITE: 'white',
* GREEN: 'green',
* } as const
*
* type Colors= ValueOf<typeof COLORS> // "black" | "white" | "green"
* ```
*/
export type ValueOf<A> = A[keyof A];
Const Contexts pitfalls
const assertion provide a level of protection against accidental mutations.
but const assertion contexts don’t make expressions fully immutable as we can see in the following example.
const arr = [1, 2, 3, 4]; // <--- this is not readonly
const foo = {
name: "foo",
otherContents: [0, 3, 4],
contents: arr,
} as const;
foo.name = "bar"; // ❌ Cannot assign to 'name' because it is read-only
foo.contents = []; // ❌ Cannot assign to 'contents' because it is read-only
foo.otherContents.push(5); // ❌...Error! Property push does not exist on type 'readonly' [0,3,4]
foo.contents.push(5); // ✅...works! 🤯
Two ways of getting a deeper level of immutability
- 1. With recurring types by utilizing the Immutable utility type.
type Immutable<T> = {
readonly [K in keyof T]: Immutable<T[K]>;
};
that recursively make every value readonly
const arr = [1, 2, 3, 4]; // <--- this is not readonly
type Foo = {
name: "foo";
otherContents: Array<number>;
contents: typeof arr;
};
const foo: Immutable<Foo> = {
name: "foo",
otherContents: [0, 3, 4],
contents: arr, // <--- now this is readonly
} as const;
foo.contents.push(5); // ❌ Error! Property push does not exist on type 'readonly' [1,2,3,4]
- 2. With const assertion
const arr = [1, 2, 3, 4] as const; // <--- this is readonly
const foo = {
name: "foo",
otherContents: [0, 3, 4],
contents: arr,
} as const;
foo.contents.push(5); // ❌ Error! Property push does not exist on type 'readonly' [1,2,3,4]
Angle Bracket Assertion Syntax
In TypeScript, we can employ the angle bracket assertion syntax to explicitly denote constant values.
let x = <const>"hello"; // '"hello"'
let y = <const>[10, 20]; // 'readonly [10, 20]'
let z = <const>{ text: "hello" }; // '{ readonly text: "hello" }'
Harnessing Template Literal Types
Template literal types combined with const assertion open up new possibilities for creativity and type safety.
export const direction = <const>["left", "right"];
type AnimationType = `${(typeof direction)[number]}-slide`; // AnimationType is "left-slide" or "right-slide"
Happy hacking !