Type Widening

Type widening is the process in which TypeScript assigns a type to a variable initialized when no type annotation was provided. It allows narrow to wider types but not vice versa. In the following example:

>tags: [[Error_TS2322]] [[Error_Assignable]]

let x = 'x'; // TypeScript infers as string, a wide type
let y: 'y' | 'x' = 'y'; // y types is a union of literal types
y = x; // Invalid Type 'string' is not assignable to type '"x" | "y"'.

TypeScript assigns string to x based on the single value provided during initialization (x), this is an example of widening.

TypeScript provides ways to have control of the widening process, for instance using “const”.

>tags: [[Const]]

Const

Using the const keyword when declaring a variable results in a narrower type inference in TypeScript.

For example:

const x = 'x'; // TypeScript infers the type of x as 'x', a narrower type
let y: 'y' | 'x' = 'y';
y = x; // Valid: The type of x is inferred as 'x'

By using const to declare the variable x, its type is narrowed to the specific literal value ‘x’. Since the type of x is narrowed, it can be assigned to the variable y without any error. The reason the type can be inferred is because const variables cannot be reassigned, so their type can be narrowed down to a specific literal type, in this case, the literal type ‘x’.

Const Modifier on Type Parameters

From version 5.0 of TypeScript, it is possible to specify the const attribute on a generic type parameter. This allows for inferring the most precise type possible. Let’s see an example without using const:

function identity<T>(value: T) {
    // No const here
    return value;
}
const values = identity({ a: 'a', b: 'b' }); // Type infered is: { a: string; b: string; }

As you can see, the properties a and b are inferred with a type of string .

Now, let’s see the difference with the const version:

function identity<const T>(value: T) {
    // Using const modifier on type parameters
    return value;
}
const values = identity({ a: 'a', b: 'b' }); // Type infered is: { a: "a"; b: "b"; }

Now we can see that the properties a and b are inferred as const, so a and b are treated as string literals rather than just string types.

Const assertion

This feature allows you to declare a variable with a more precise literal type based on its initialization value, signifying to the compiler that the value should be treated as an immutable literal. Here are a few examples:

On a single property:

const v = {
    x: 3 as const,
};
v.x = 3;

On an entire object:

const v = {
    x: 1,
    y: 2,
} as const;

This can be particularly useful when defining the type for a tuple:

const x = [1, 2, 3]; // number[]
const y = [1, 2, 3] as const; // Tuple of readonly [1, 2, 3]