Mac station with a “Do more” wallpaper. Photo by Carl Heyerdahl.
Photo by Carl Heyerdahl on Unsplash

Exploring TypeScript’s Type System

Part 3: TypeScript Types, Type Aliases, and Assertions

Irene Smolchenko
Bits and Pieces
Published in
12 min readMay 28, 2023

--

In the previous article, we discussed the importance of type annotations and explored the use of primitive types in TypeScript. Now, let’s take a closer look at specific types in TypeScript, and discuss type aliases and type assertions. By gaining a solid understanding of these, we can enhance the precision and reliability of our TypeScript code.

TypeScript Types

The any type

The any type is a special type that represents a dynamic or unknown value. It is often used when the developer wants to opt-out of type checking for a particular variable or when dealing with values of different types within the same context. The any type essentially disables static type checking for that variable, allowing it to have any value and participate in any operation without throwing type errors.
Here are a few examples to illustrate the usage of the any type:

// 1. Declaring a variable with the any type:

let myVariable: any;
myVariable = 5; // Valid
myVariable = "Hello"; // Valid
myVariable = true; // Valid
// 2. Using any with arrays and functions:

let myArray: any[] = [1, "two", true]; // An array with mixed types

function processData(data: any): void {
// Perform operations on the data of any type
}

processData(10); // Valid
processData("Hello"); // Valid
// 3. Interacting with JavaScript libraries:

// When interacting with a JavaScript library, the return type might be unknown
declare var externalLibrary: any;

const result = externalLibrary.someFunction(); // No type checking on result

While the any type can be useful in certain scenarios, it's generally recommended to use it sparingly. The overuse of any can lead to decreased type safety and eliminate the benefits of TypeScript's static typing.

The void type

The void type is used to indicate the absence of any type. It is often used as the return type of functions that do not explicitly return a value. Variables of type void can only have the values undefined or null.
Here are a few examples to illustrate the usage of the void type:

// 1. Void as a return type: can help enforce that certain functions are used 
// for their side effects rather than their return values.

function sayHello(): void {
console.log("Hello!");
}

const result: void = sayHello();
// 2. Void variables:

let unusable: void;
unusable = undefined; // Valid
unusable = null; // Valid

The never type

The never type represents the type of values that never occur. It is used to indicate situations where a function never returns or when a type cannot have any possible value. The never type can be assigned to any type but no type can be assigned to never. See the examples below to illustrate the usage of the never type:

// 1. The function throws an error and never completes normally. 
// Its return type is 'never' because it never returns a value.

function throwError(message: string): never {
throw new Error(message);
}

// 2. The function enters an infinite loop and never terminates.
// It also has a return type of 'never' because it never returns a value.

function infiniteLoop(): never {
while (true) {
// Infinite loop
}
}

It’s important to note that while never represents the absence of a value, it's distinct from void. void indicates the absence of a meaningful value, while never represents a value that can never occur.

Null and Undefined types, and Strict Null checks

As mentioned in the previous article, null and undefined are used to represent the absence of a value.
null — represents the intentional absence of any object value. It can be assigned to variables to indicate that they have no meaningful value.
undefined— represents the absence of a value. It typically occurs when a variable has been declared but has not been assigned a value.

Strict Null Checks: TypeScript provides a compiler option called “strictNullChecks” to enable strict null checking. When it’s enabled, TypeScript introduces a distinction between nullable and non-nullable types. Strict null checks help catch potential null or undefined errors at compile-time and promote safer and more robust code.

let phoneNumber: string; // Non-nullable variable
phoneNumber = "1234567890"; // Valid
phoneNumber = null; // Error (with strict null checks enabled)
phoneNumber = undefined; // Error (with strict null checks enabled)

To enable the strictNullChecks compiler option, modify your tsconfig.json file, save it, and recompile your TypeScript code for the changes to take effect.

{
"compilerOptions": {
"strictNullChecks": true
}
}

Union and Literal Types

Union and literal types are powerful features that allow you to work with a set of specific values.

  • Union Types: allow you to declare a variable that can hold values of different types. You can combine multiple types using the union operator (|).
    They are useful when you have a scenario where a variable can accept multiple types or when working with functions that can accept different parameter types.
let myVariable: number | string; // The variable can hold either a value of types number or string

myVariable = 10; // Valid, assigning a number
myVariable = "Hello"; // Valid, assigning a string
  • Literal Types: allow you to specify exact values and narrow down the possible values that a variable can hold.
let direction: "up" | "down" | "left" | "right"; // declared with a literal type that can only hold one of the specified values

direction = "up"; // Valid
direction = "left"; // Valid
direction = "forward"; // Error: 'forward' is not assignable to type "up" | "down" | "left" | "right"

You can also combine union and literal types to create more specific and flexible type annotations:

let result: "success" | "failure" | number;

result = "success"; // Valid
result = "failure"; // Valid
result = 42; // Valid
result = "error"; // Error: 'error' is not assignable to type "success" | "failure" | number

Function types

Function types allow you to define the type signature of a function and specify the type of a function, including its parameter types and return type, using a type annotation. This enables TypeScript to perform type checking on functions and ensure type safety when calling or defining functions.

To define a function type, you can specify the parameter types in parentheses followed by an arrow (=>) and the return type.

type AddFunction = (a: number, b: number) => number;

const add: AddFunction = (a, b) => {
return a + b;
};

In this example, we define a function type AddFunction that represents a function that takes two parameters of type number and returns a value of type number. The add function is then assigned the AddFunction type, ensuring that it conforms to the specified parameter and return types.

You can also use function types as part of other type annotations, such as object types.

type GreetingFunction = (name: string) => void;

type Greeter = {
greet: GreetingFunction;
};

const englishGreeter: Greeter = {
greet: (name) => {
console.log(`Hello, ${name}!`);
},
};

const spanishGreeter: Greeter = {
greet: (name) => {
console.log(`¡Hola, ${name}!`);
},
};

englishGreeter.greet("John"); // Output: Hello, John!
spanishGreeter.greet("Carlos"); // Output: ¡Hola, Carlos!

Typed Functions and Default Params

Typed Functions are functions that have explicit type annotations for their parameters and return types. By providing type annotations, you are specifying the expected types of the function’s arguments and its return value.

function add(a: number, b: number): number {
return a + b;
}

Default Params allow you to assign default values to function parameters when no value or an undefined value is provided during the function call. If an argument is provided, it overrides the default value.

function greet(name: string = "Guest"): void {
console.log(`Hello, ${name}!`);
}

greet(); // Output: Hello, Guest!
greet("John"); // Output: Hello, John!

Object Types

Object types allow you to define the shape or structure of an object, specifying the types of its keys and their values.
To define an object type using the {} Syntax:

// Define an object type
// Use 'let', only when you need to define the object type for a specific variable or a small scope
let person: {
name: string;
age: number;
email: string;
};

// assign an object that matches this type
person = {
name: "John",
age: 30,
email: "john@example.com",
};

To define an object type using Record<string, Type>:

// By using 'type' we can reuse the type
type Person = Record<string, string | number>;

let user: Person = {
name: "Alice",
age: 25,
email: "alice@example.com",
};

In this example, we define a type Person using Record<string, Type>, where the keys are of type string, and the values can be either string or number. We then create an object user that adheres to this type, ensuring that the properties have the expected types.

Object types can also include optional properties denoted by ? and readonly properties denoted by readonly.

type Book = {
title: string;
author: string;
year: number;
pages?: number; // Optional property
readonly ISBN: string; // Readonly property
};

// When creating an object of type Book, we can assign values to all properties except the ISBN
let myBook: Book = {
title: "The TypeScript Handbook",
author: "John Doe",
year: 2022,
ISBN: "978-1234567890",
};

Array Types and Generics

Array types and generics provide ways to define and work with arrays of specific types.

Array types allow you to specify the type of elements that an array can contain. They not only enable you to perform operations on arrays, such as working with array methods and accessing array elements, but also leverage TypeScript’s type checking capabilities to ensure type safety during these operations.

let numbers: number[] = [1, 2, 3, 4, 5]; // can only contain elements of type number
let names: string[] = ["Alice", "Bob", "Charlie"]; // can only contain elements of type string

Generics provide a way to create reusable components and functions that can work with a variety of types. When applied to arrays, generics allow you to define arrays that can hold elements of any specified type.

function printArray<T>(array: T[]): void {
for (let item of array) {
console.log(item);
}
}

let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

printArray(numbers); // Prints each number
printArray(names); // Prints each name

In this example, we define a generic function printArray that takes an array of type T as a parameter and prints each item in the array. The type parameter T allows the function to work with arrays of any type. We then invoke the function with both the numbers and names arrays, and TypeScript infers the types based on the passed arguments.

Tuple Types for Arrays

Tuple types allow you to define arrays with fixed lengths and specify the types of each element at specific positions within the array. Tuple types provide a way to represent and work with arrays that have a known and consistent element structure. Examples:

// 1. Basic Tuple Type
let employee: [string, number, boolean]; // define a tuple type that consists of three elements for the employee variable
employee = ["John Doe", 30, true]; // assign an array with specific element types to the employee variable
// 2. Accessing Tuple Elements
let employee: [string, number, boolean] = ["John Doe", 30, true];

let name: string = employee[0];
let age: number = employee[1];
let isActive: boolean = employee[2];

console.log(`Name: ${name}, Age: ${age}, Active: ${isActive}`);
// 3. Array Destructuring with Tuples
let employee: [string, number, boolean] = ["John Doe", 30, true];

// TypeScript automatically assigns the corresponding tuple elements based on their positions.
// Each element has a predetermined type at a specific position
let [name, age, isActive] = employee;

console.log(`Name: ${name}, Age: ${age}, Active: ${isActive}`);

💡 What if you needed to reuse these types across all your projects? An open-source toolchain such as Bit can help you share types across multiple projects, considerably minimizing boilerplate.

Type Aliases

Type aliases allow you to create custom names for existing types. They provide a way to define reusable type definitions and improve code readability. With type aliases, you can create a new name for any valid TypeScript type, including primitive types, object types, union types, and more.

To define a type alias, you can use the type keyword followed by the desired name and the corresponding type definition. Here are a few examples to illustrate the usage of type aliases:

// Object types
type Point = { // define a type alias Point that represents an object with x and y properties of type number
x: number;
y: number;
};

const origin: Point = { x: 0, y: 0 };
const myPoint: Point = { x: 5, y: 10 };
// Parameterized type aliases
type Pair<T> = {
first: T;
second: T;
};

// Allowing to create generic types that can be reused with different type arguments.
const myPair: Pair<number> = { first: 1, second: 2 };
const stringPair: Pair<string> = { first: "hello", second: "world" };

In the last example, we define a generic type alias Pair<T> that represents a pair of values of the same type. The T parameter allows us to specify the type of the values when using the alias.

Type Assertions

Type assertions allow you to explicitly override the inferred type of a value. They are a way to tell the TypeScript compiler that you know more about the type of a variable than it can infer automatically. Type assertions are typically used when you have additional knowledge about the types that TypeScript cannot deduce on its own.

Type assertions come in two forms: “angle-bracket” syntax and the as syntax.

// "Angle-Bracket" Syntax:
let myValue: any = "Hello, TypeScript!"; // initially assigned the type 'any'
let stringLength: number = (<string>myValue).length; // we explicitly assert that myValue should be treated as a string.

console.log(stringLength); // Output: 19
// 'as' Syntax:
let myValue: any = "Hello, TypeScript!";
let stringLength: number = (myValue as string).length;

console.log(stringLength); // Output: 19

If the asserted type is incompatible with the actual value’s type, a type error may occur at runtime.

Type assertions should be used with caution, as they bypass some of TypeScript’s type safety checks. It’s generally recommended to use them only when you have confidence in the actual type of a value and when you’re certain that the assertion won’t lead to runtime errors.

Conclusion

TypeScript’s type system, with its rich features like types, type aliases, and type assertions, provides a solid foundation for writing safer and more maintainable code. We explored how these tools allow us to specify and manipulate types effectively.

In the next article, we will move along to interfaces, classes, properties, and inheritance. Interfaces will enable us to define contracts and establish clear object shapes. Classes, with their object-oriented nature, will empower us to build reusable and structured code. We will also explore the concept of properties and how inheritance facilitates code organization and reuse.

By buying me a virtual croissant on Buy Me a Coffee, you can directly support my creative journey. Your contribution helps me continue creating high-quality content. Thank you for your support!

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

🍴🛌🏻 👩🏻‍💻 🔁 Front End Web Developer | Troubleshooter | In-depth Tech Writer | 🦉📚 Duolingo Streak Master | ⚛️ React | 🗣️🤝 Interviews Prep