Home Blog Page 35

Custom User-Defined Type Guards

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are User-Defined Type Guards?
  • Syntax of User-Defined Type Guards
  • Why Use Custom Type Guards?
  • Creating Custom Type Guards
    • Example 1: Checking for Specific Properties in an Object
    • Example 2: Complex Type Guards for Unions
  • Using Type Guards in Conditional Statements
  • When to Use Custom Type Guards
  • Conclusion

Introduction

TypeScript is designed to be both flexible and robust, providing developers with a powerful type system. One of the key features of TypeScript is type guards, which allow you to refine and narrow down the type of a variable within a certain scope. While TypeScript provides built-in type guards like typeof and instanceof, there are times when you might want to create your own custom user-defined type guards.

Custom type guards are particularly useful when you need to check for more complex types or specific conditions that are not easily handled by the built-in type guards. By using user-defined type guards, you can ensure type safety in cases where TypeScript cannot automatically infer the correct type.

In this article, we will explore how to create and use custom user-defined type guards in TypeScript.


What Are User-Defined Type Guards?

A user-defined type guard is a TypeScript function that returns a boolean value and tells TypeScript the specific type of an object or variable. These type guards help TypeScript narrow the type of a variable based on runtime checks, ensuring that the correct type is inferred within a block of code.

A user-defined type guard function follows a specific pattern, where it includes a type predicate in the return type. This type predicate is a special syntax that tells TypeScript what type the function is narrowing to.


Syntax of User-Defined Type Guards

The syntax for a custom user-defined type guard looks like this:

function isSomeType(obj: any): obj is SomeType {
// Check for properties or conditions to determine if the object is of type 'SomeType'
return obj !== null && typeof obj === 'object' && 'propertyName' in obj;
}

Here:

  • obj is SomeType is a type predicate that tells TypeScript that the function will return true only if the obj is of type SomeType. This allows TypeScript to narrow the type of obj within the conditional block where the guard is used.

Why Use Custom Type Guards?

While TypeScript provides built-in type guards like typeof and instanceof, there are cases where these aren’t enough. For example:

  • You may need to check if an object has a specific property or method.
  • You may want to perform complex checks on union types.
  • You might be working with types that are difficult to distinguish without custom logic (e.g., checking whether a variable is an instance of one of several classes).

By creating custom user-defined type guards, you can fine-tune the type checking process, ensuring type safety even in the most complex scenarios.


Creating Custom Type Guards

Example 1: Checking for Specific Properties in an Object

Let’s say you have a union type, and you need to narrow the type based on whether an object has a particular property.

interface Cat {
name: string;
breed: string;
}

interface Dog {
name: string;
barkSound: string;
}

type Animal = Cat | Dog;

// User-defined type guard to check if an animal is a Cat
function isCat(animal: Animal): animal is Cat {
return (animal as Cat).breed !== undefined;
}

// Using the custom type guard
const pet: Animal = { name: "Whiskers", breed: "Siamese" };

if (isCat(pet)) {
console.log(`${pet.name} is a cat of breed ${pet.breed}`);
} else {
console.log(`${pet.name} is a dog that barks like ${pet.barkSound}`);
}

In this example, isCat is a user-defined type guard that checks if the animal object has a breed property. By using this function, TypeScript can narrow the type of pet to Cat inside the if block, allowing safe access to the breed property.

Example 2: Complex Type Guards for Unions

In complex applications, you may have a union type with multiple shapes or interfaces, and you may need to differentiate between them based on several properties.

interface Bird {
fly(): void;
name: string;
}

interface Fish {
swim(): void;
name: string;
}

type Animal = Bird | Fish;

// User-defined type guard to check if an animal can fly
function isBird(animal: Animal): animal is Bird {
return (animal as Bird).fly !== undefined;
}

const petFish: Animal = { name: "Goldy", swim: () => console.log("Swim!") };
const petBird: Animal = { name: "Tweety", fly: () => console.log("Fly!") };

if (isBird(petFish)) {
petFish.fly(); // Error: petFish does not have a fly method
} else {
petFish.swim(); // Correct: petFish has swim method
}

if (isBird(petBird)) {
petBird.fly(); // Correct: petBird can fly
}

In this case, isBird is a user-defined type guard that checks if the animal can fly. TypeScript will narrow the type of pet inside the if block to Bird when the fly method is present.


Using Type Guards in Conditional Statements

Custom user-defined type guards are most useful when working with conditional statements. Once you define a custom type guard, you can use it to safely narrow types within if or switch blocks.

For example:

function processAnimal(animal: Animal) {
if (isBird(animal)) {
animal.fly(); // TypeScript knows animal is of type Bird
} else {
animal.swim(); // TypeScript knows animal is of type Fish
}
}

In this example, the custom type guard isBird is used to narrow down the type of animal and ensure that the correct method is called for either a bird or a fish.


When to Use Custom Type Guards

You should consider using custom type guards when:

  1. You have a complex union type and need to differentiate between types based on runtime properties or conditions.
  2. Built-in type guards are not sufficient (e.g., when you need to check the presence of specific properties or perform logic).
  3. You work with class hierarchies or interfaces and need to differentiate between different classes or object shapes.
  4. You want to improve the safety and maintainability of your code by making type checks more explicit and precise.

Conclusion

Custom user-defined type guards are a powerful feature of TypeScript that allow you to refine and narrow types based on runtime checks. By creating your own type guards, you can improve type safety, especially in scenarios that involve complex types, union types, or class hierarchies.

By defining functions that return a boolean and include a type predicate, TypeScript is able to infer the exact type of a variable in specific conditions, enabling safer, more reliable code. Whether you’re checking for properties in objects, dealing with unions, or working with classes, custom type guards give you full control over how types are checked and refined.

Built-in Type Guards: typeof, instanceof

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are Type Guards in TypeScript?
  • typeof Type Guard
    • Definition and Syntax
    • Use Cases and Examples
  • instanceof Type Guard
    • Definition and Syntax
    • Use Cases and Examples
  • Practical Examples
    • Example 1: Using typeof to Narrow Types
    • Example 2: Using instanceof to Narrow Types
  • When and Why to Use Type Guards
  • Conclusion

Introduction

TypeScript is a statically-typed superset of JavaScript, and it provides powerful type-checking features that improve the reliability of your code. One of the most helpful features of TypeScript is type guards, which allow you to narrow down the type of a variable within a certain block of code. This is particularly useful when you’re working with unions or any type, and you want to check the type of a value before performing specific operations.

In this article, we’ll focus on two built-in type guards in TypeScript: typeof and instanceof. These are simple yet powerful tools that enable you to check and narrow the types of variables effectively.


What Are Type Guards in TypeScript?

Type guards are expressions that allow TypeScript to narrow down the type of a variable within a certain scope. TypeScript uses these guards to refine the type of a variable when the code runs, allowing for more precise type checking and safer operations.

Type guards can be built-in (like typeof and instanceof), user-defined (such as custom functions that return a boolean), or even inferred by TypeScript itself based on context.

In this article, we will specifically focus on the two most commonly used built-in type guards: typeof and instanceof.


typeof Type Guard

Definition and Syntax

The typeof operator is used to determine the type of a primitive value (e.g., number, string, boolean, etc.). In TypeScript, typeof can also be used as a type guard to narrow down the type of a variable based on its value.

The syntax for typeof is:

if (typeof variable === "type") {
// TypeScript knows the type of 'variable' inside this block
}

Here, the typeof operator checks the runtime type of the variable, and if it matches the specified type (e.g., "string", "number", "boolean", etc.), TypeScript narrows the type of that variable.

Use Cases and Examples

  • Primitives: typeof is particularly useful when you are working with primitive types, such as string, number, boolean, symbol, undefined, and object. It can help refine the type when you deal with union types or variables of type any.

Example:

function printValue(value: number | string) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // Here, TypeScript knows 'value' is a string
} else if (typeof value === "number") {
console.log(value.toFixed(2)); // Here, 'value' is narrowed to a number
}
}

printValue("hello"); // Output: HELLO
printValue(123.456); // Output: 123.46

In this example, typeof is used to check if value is a string or number, and based on that, TypeScript knows the exact type of value inside each conditional block, allowing you to safely call methods specific to each type (e.g., toUpperCase for strings and toFixed for numbers).


instanceof Type Guard

Definition and Syntax

The instanceof operator is used to check if an object is an instance of a specific class or constructor function. It’s particularly useful when you want to narrow the type of a class-based object.

The syntax for instanceof is:

if (variable instanceof Class) {
// TypeScript narrows the type of 'variable' to 'Class' inside this block
}

Here, instanceof checks whether variable is an instance of the Class. If it is, TypeScript narrows the type of variable to that class type, allowing access to class-specific properties and methods.

Use Cases and Examples

  • Class Instances: instanceof is used to check whether an object is an instance of a particular class, allowing you to work with class-specific properties and methods without TypeScript complaining about incorrect types.
  • Inheritance: instanceof can also be used to check if an object is an instance of a subclass, making it easy to handle objects that inherit from a common base class.

Example:

class Car {
constructor(public model: string, public year: number) {}
}

class Bike {
constructor(public brand: string, public type: string) {}
}

function printVehicleInfo(vehicle: Car | Bike) {
if (vehicle instanceof Car) {
console.log(`Car Model: ${vehicle.model}, Year: ${vehicle.year}`);
} else if (vehicle instanceof Bike) {
console.log(`Bike Brand: ${vehicle.brand}, Type: ${vehicle.type}`);
}
}

const myCar = new Car('Toyota', 2021);
const myBike = new Bike('Yamaha', 'Sport');

printVehicleInfo(myCar); // Output: Car Model: Toyota, Year: 2021
printVehicleInfo(myBike); // Output: Bike Brand: Yamaha, Type: Sport

In this example, instanceof is used to check whether the vehicle is an instance of Car or Bike, and TypeScript narrows the type accordingly inside each if block. This ensures type-safe access to properties specific to each class.


Practical Examples

Example 1: Using typeof to Narrow Types

function processValue(value: string | number) {
if (typeof value === "string") {
console.log(value.length); // 'value' is narrowed to a string here
} else {
console.log(value.toFixed(2)); // 'value' is narrowed to a number here
}
}

processValue("hello"); // Output: 5
processValue(123.456); // Output: 123.46

In this example, typeof allows TypeScript to refine the type of value based on whether it’s a string or a number, enabling safe method calls like length for strings and toFixed for numbers.

Example 2: Using instanceof to Narrow Types

class Shape {
constructor(public name: string) {}
}

class Circle extends Shape {
constructor(name: string, public radius: number) {
super(name);
}
}

class Rectangle extends Shape {
constructor(name: string, public width: number, public height: number) {
super(name);
}
}

function calculateArea(shape: Shape) {
if (shape instanceof Circle) {
return Math.PI * shape.radius ** 2; // Narrowed to Circle type
} else if (shape instanceof Rectangle) {
return shape.width * shape.height; // Narrowed to Rectangle type
} else {
throw new Error("Unknown shape");
}
}

const circle = new Circle("Circle", 10);
const rectangle = new Rectangle("Rectangle", 20, 30);

console.log(calculateArea(circle)); // Output: 314.1592653589793
console.log(calculateArea(rectangle)); // Output: 600

In this example, instanceof is used to narrow the type of shape to Circle or Rectangle, allowing the correct formula to be applied based on the shape type.


When and Why to Use Type Guards

  • typeof: Use typeof when dealing with primitive types such as strings, numbers, and booleans, or when working with union types. It is a simple and effective way to check the type at runtime.
  • instanceof: Use instanceof when working with class-based types or when dealing with inheritance. It allows TypeScript to infer and narrow the type of objects that are instances of specific classes, ensuring type safety for class methods and properties.

Both typeof and instanceof are integral tools for narrowing down types in TypeScript, especially when working with complex or dynamic data structures.


Conclusion

In this article, we’ve explored two powerful built-in type guards in TypeScript: typeof and instanceof. These operators allow you to narrow the types of variables within specific blocks of code, enabling type-safe operations and reducing runtime errors.

By using typeof for primitive types and instanceof for class instances, you can handle dynamic data structures more effectively and ensure that your code adheres to TypeScript’s type safety features.

Utility Types Part 3: ReturnType, Parameters, ConstructorParameters, InstanceType

0
typscript course
typscript course

Table of Contents

  • Introduction
  • Overview of Utility Types in TypeScript
  • ReturnType<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Parameters<T>
    • Definition and Syntax
    • Use Cases and Examples
  • ConstructorParameters<T>
    • Definition and Syntax
    • Use Cases and Examples
  • InstanceType<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Practical Examples
    • Example 1: Using ReturnType to Infer Return Types
    • Example 2: Using Parameters to Extract Function Parameters
    • Example 3: Using ConstructorParameters for Class Constructor Types
    • Example 4: Using InstanceType to Get Instance Types from Classes
  • When and Why to Use These Utility Types
  • Conclusion

Introduction

In this article, we continue our deep dive into TypeScript’s powerful utility types. After exploring types like Pick, Omit, and Record in the previous articles, we now turn our attention to four more utility types: ReturnType, Parameters, ConstructorParameters, and InstanceType. These types focus on working with functions and classes, allowing you to extract and manipulate function return types, parameter types, constructor parameter types, and class instance types.

These utility types are invaluable when working with dynamic code that depends on the structure of existing functions or classes, especially when you want to infer types automatically from them.


Overview of Utility Types in TypeScript

In earlier articles, we explored utility types like Partial, Required, and Readonly, which modify properties of types, as well as Pick, Omit, and Record, which help in selecting, omitting, and mapping types. The utility types we’ll cover in this article are particularly useful for working with functions and class constructors:

  • ReturnType<T>: Extracts the return type of a function.
  • Parameters<T>: Extracts the types of parameters from a function.
  • ConstructorParameters<T>: Extracts the types of parameters from a class constructor.
  • InstanceType<T>: Extracts the type of an instance from a class type.

ReturnType<T>

Definition and Syntax

ReturnType<T> is a utility type that allows you to extract the return type of a function type. It infers the type that the function returns, which can be useful when you want to avoid manually specifying the return type.

The syntax for ReturnType is:

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;

Here, T is the function type, and R represents the inferred return type. If T is not a valid function type, ReturnType will return never.

Use Cases and Examples

  • Type inference: You can use ReturnType when you want to infer the return type of a function without explicitly specifying it.
  • Dynamic function analysis: When working with higher-order functions or callbacks, ReturnType can automatically infer the return type.

Example:

function getUser(id: number) {
return { id, name: 'John Doe' };
}

type UserReturnType = ReturnType<typeof getUser>;

const user: UserReturnType = {
id: 1,
name: 'John Doe',
};

console.log(user); // { id: 1, name: 'John Doe' }

In this example, ReturnType<typeof getUser> extracts the return type of the getUser function, which is an object with id and name. This helps us ensure type safety when using the returned value from the function.


Parameters<T>

Definition and Syntax

The Parameters<T> utility type extracts the types of the parameters of a function type T. This is particularly useful when you need to reference the types of the parameters from an existing function type, without needing to manually define them.

The syntax for Parameters is:

type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;

In this definition, T is the function type, and P is the inferred type of the function’s parameters.

Use Cases and Examples

  • Extracting function arguments: Parameters is useful when you need to access or reuse the parameter types of a function, for example, in function decorators or higher-order functions.
  • Building function signatures: It helps in situations where you need to pass a function’s parameters as types to another function or component.

Example:

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

type SumParameters = Parameters<typeof sum>;

const args: SumParameters = [1, 2];

console.log(sum(...args)); // 3

Here, Parameters<typeof sum> extracts the parameters of the sum function, which is [number, number]. This allows us to create an args array with the correct types and pass it to the function safely.


ConstructorParameters<T>

Definition and Syntax

The ConstructorParameters<T> utility type extracts the types of the parameters of a class constructor. It’s particularly useful when working with class-based patterns or factory functions where you want to reference constructor parameters.

The syntax for ConstructorParameters is:

type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;

In this definition:

  • T is a class constructor type.
  • P is the inferred type of the constructor’s parameters.

Use Cases and Examples

  • Type-safe class instantiation: ConstructorParameters helps when you need to extract the constructor parameter types from a class to instantiate it correctly or use it in other contexts.
  • Creating factory functions: It is useful when building factory functions that accept class constructor arguments and return instances of classes.

Example:

class Person {
constructor(public name: string, public age: number) {}
}

type PersonConstructorParams = ConstructorParameters<typeof Person>;

const personArgs: PersonConstructorParams = ['Alice', 30];
const person = new Person(...personArgs);

console.log(person); // Person { name: 'Alice', age: 30 }

In this example, ConstructorParameters<typeof Person> extracts the types of the constructor parameters of the Person class. This allows us to create an array of arguments with the correct types and instantiate the class safely.


InstanceType<T>

Definition and Syntax

InstanceType<T> is a utility type that extracts the type of an instance created by a class constructor. It’s useful when you want to refer to the type of an object created from a class without manually specifying it.

The syntax for InstanceType is:

type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : never;

In this definition:

  • T is the class constructor type.
  • R represents the type of the instance that the class constructor creates.

Use Cases and Examples

  • Extracting instance types: InstanceType is helpful when you need to infer the type of an instance from a class constructor.
  • Working with factory functions: When working with functions that return instances of classes, InstanceType allows you to extract and use the type of the instance.

Example:

class Car {
constructor(public model: string, public year: number) {}
}

type CarInstance = InstanceType<typeof Car>;

const car: CarInstance = new Car('Toyota', 2022);

console.log(car); // Car { model: 'Toyota', year: 2022 }

In this example, InstanceType<typeof Car> extracts the type of an instance created by the Car class. This allows us to type the car object correctly without manually specifying the type.


Practical Examples

Example 1: Using ReturnType to Infer Return Types

function multiply(a: number, b: number): number {
return a * b;
}

type MultiplyReturnType = ReturnType<typeof multiply>;

const result: MultiplyReturnType = 20;

Here, ReturnType extracts the return type of the multiply function (number), making it easier to work with the result without manually specifying the type.

Example 2: Using Parameters to Extract Function Parameters

function greet(message: string, name: string): string {
return `${message}, ${name}!`;
}

type GreetParams = Parameters<typeof greet>;

const args: GreetParams = ['Hello', 'World'];

In this example, Parameters<typeof greet> extracts the parameters of the greet function, which allows us to safely pass the arguments as a tuple.

Example 3: Using ConstructorParameters for Class Constructor Types

class Animal {
constructor(public species: string, public sound: string) {}
}

type AnimalConstructorParams = ConstructorParameters<typeof Animal>;

const animalArgs: AnimalConstructorParams = ['Dog', 'Bark'];
const animal = new Animal(...animalArgs);

Here, ConstructorParameters<typeof Animal> extracts the constructor parameter types of the Animal class.

Example 4: Using InstanceType to Get Instance Types from Classes

class Book {
constructor(public title: string, public author: string) {}
}

type BookInstance = InstanceType<typeof Book>;

const book: BookInstance = new Book('1984', 'George Orwell');

In this case, InstanceType<typeof Book> infers the type of an instance of the Book class.


When and Why to Use These Utility Types

  • ReturnType: Use it when you want to infer and use the return type of a function without manually specifying it.
  • Parameters: Use it when you need to extract and work with the parameter types of a function.
  • ConstructorParameters: Use it when you need to reference the types of a class constructor’s parameters.
  • InstanceType: Use it when you want to extract the instance type of a class without manually specifying it.

These utility types save time and reduce errors by allowing TypeScript to infer types automatically, ensuring type safety and consistency across your code.


Conclusion

In this article, we’ve explored four powerful utility types in TypeScript: ReturnType, Parameters, ConstructorParameters, and InstanceType. These utility types are invaluable when working with functions and classes, as they allow you to dynamically extract and infer types based on existing code. By using these types effectively, you can write more flexible and maintainable code with TypeScript’s strong type system.

By mastering these utility types, you’ll enhance your ability to create type-safe and dynamic applications that take full advantage of TypeScript’s type inference capabilities.

Utility Types Part 2: Pick, Omit, Record

0
typscript course
typscript course

Table of Contents

  • Introduction
  • Overview of Utility Types in TypeScript
  • Pick<T, K>
    • Definition and Syntax
    • Use Cases and Examples
  • Omit<T, K>
    • Definition and Syntax
    • Use Cases and Examples
  • Record<K, T>
    • Definition and Syntax
    • Use Cases and Examples
  • Practical Examples
    • Example 1: Using Pick to Select Specific Properties
    • Example 2: Using Omit to Exclude Specific Properties
    • Example 3: Using Record for Dynamic Key-Value Mapping
  • When and Why to Use These Utility Types
  • Conclusion

Introduction

TypeScript’s utility types continue to provide powerful features that allow developers to manipulate and shape types based on specific needs. In this article, we’ll explore three more utility types: Pick, Omit, and Record. These utility types provide a way to create new types from existing ones by selecting specific properties, excluding properties, or defining object types with dynamic keys.

These types are often used in scenarios where you need to create subsets of types, exclude specific properties, or map values in an object. Understanding when and how to use these utility types can help you create more flexible, type-safe code.


Overview of Utility Types in TypeScript

In Part 1 of this series, we covered the Partial, Required, and Readonly utility types, which allow you to modify properties of a type by making them optional, required, or read-only.

In this part, we’ll look at three additional utility types:

  • Pick<T, K>: Creates a new type by picking a subset of properties from another type.
  • Omit<T, K>: Creates a new type by omitting specific properties from another type.
  • Record<K, T>: Creates an object type with keys of type K and values of type T.

These utility types help in extracting or excluding properties dynamically, which is useful when working with objects and customizing types for specific cases.


Pick<T, K>

Definition and Syntax

The Pick<T, K> utility type allows you to create a new type by selecting a subset of properties from an existing type. You specify which properties to include by passing a union of property names K. This makes Pick useful when you only need a specific set of properties from a larger type.

The syntax for Pick is:

type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

In this definition:

  • T is the original type.
  • K is a subset of the keys from T (i.e., a union of property names).
  • The result is a new type with only the selected properties.

Use Cases and Examples

  • Selecting specific properties: When you need to create a new type that includes only certain properties from an existing type.
  • Creating simplified types: When you only need a subset of an object’s properties for a particular use case, such as in API calls or UI components.

Example:

interface User {
id: number;
name: string;
email: string;
age: number;
}

type UserInfo = Pick<User, 'name' | 'email'>;

const userInfo: UserInfo = {
name: 'Alice',
email: '[email protected]',
};

console.log(userInfo); // { name: 'Alice', email: '[email protected]' }

In this example, the UserInfo type only contains the name and email properties of the User type. This is useful when you need to pass only certain properties, such as in a scenario where only user details like name and email are required.


Omit<T, K>

Definition and Syntax

The Omit<T, K> utility type creates a new type by excluding specific properties from another type. You specify which properties to exclude by passing a union of property names K. This is useful when you want to remove certain properties from an object while retaining the rest.

The syntax for Omit is:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

In this definition:

  • T is the original type.
  • K is a union of property names to exclude from T.
  • The result is a new type that includes all properties of T except those specified in K.

Use Cases and Examples

  • Excluding specific properties: When you need to create a new type that excludes certain properties, such as when working with partial data or avoiding sensitive information.
  • Modifying types dynamically: When you need to create variations of an object type with certain properties removed.

Example:

interface Product {
id: number;
name: string;
description: string;
price: number;
}

type ProductPreview = Omit<Product, 'description' | 'price'>;

const productPreview: ProductPreview = {
id: 1,
name: 'Laptop',
};

console.log(productPreview); // { id: 1, name: 'Laptop' }

In this example, the ProductPreview type omits the description and price properties, making it a simplified version of the Product type. This could be useful when you need to display a preview of a product but don’t require all the details.


Record<K, T>

Definition and Syntax

The Record<K, T> utility type creates an object type with keys of type K and values of type T. It is useful when you need to create a mapping of keys to values, particularly when the keys are known beforehand or can be dynamically defined.

The syntax for Record is:

type Record<K extends keyof any, T> = {
[P in K]: T;
};

In this definition:

  • K is the type of the keys. It must extend keyof any, meaning it can be a string, number, or symbol.
  • T is the type of the values associated with each key.

Use Cases and Examples

  • Dynamic key-value pairs: When you need to define an object type with a dynamic set of keys and values.
  • Mapping keys to specific types: When creating a mapping of known keys to specific values, such as representing a set of states or configurations.

Example:

type Role = 'admin' | 'user' | 'guest';

type Permissions = Record<Role, boolean>;

const rolePermissions: Permissions = {
admin: true,
user: false,
guest: false,
};

console.log(rolePermissions); // { admin: true, user: false, guest: false }

In this example, the Permissions type is created with Record<Role, boolean>, meaning the object has keys that are one of the Role values (admin, user, guest), and each key is associated with a boolean value.


Practical Examples

Example 1: Using Pick to Select Specific Properties

You can use Pick when you need to select only a subset of properties from a larger object, such as when displaying certain fields in a UI component:

interface Employee {
id: number;
name: string;
department: string;
salary: number;
}

type EmployeeOverview = Pick<Employee, 'id' | 'name'>;

const employeeOverview: EmployeeOverview = {
id: 1,
name: 'John Doe',
};

Example 2: Using Omit to Exclude Specific Properties

You can use Omit when you want to exclude certain properties from an object:

interface Task {
id: number;
title: string;
description: string;
status: string;
}

type TaskSummary = Omit<Task, 'description'>;

const taskSummary: TaskSummary = {
id: 1,
title: 'Complete TypeScript tutorial',
status: 'In Progress',
};

Example 3: Using Record for Dynamic Key-Value Mapping

You can use Record to create a dynamic mapping of keys to values:

type CountryCode = 'US' | 'CA' | 'MX';

type CountryNames = Record<CountryCode, string>;

const countries: CountryNames = {
US: 'United States',
CA: 'Canada',
MX: 'Mexico',
};

When and Why to Use These Utility Types

  • Pick: Use Pick when you need to select a subset of properties from an existing type. This is useful when you only need specific data from an object and want to avoid unnecessary properties.
  • Omit: Use Omit when you want to exclude specific properties from a type. This is particularly useful when working with partial data or sensitive information that you don’t want to expose.
  • Record: Use Record when you need to create an object type with a dynamic set of keys and values. This is useful when building maps, dictionaries, or dynamic objects based on known key types.

Conclusion

In this second part of our exploration into TypeScript’s utility types, we’ve learned about Pick, Omit, and Record. These utility types give you the ability to manipulate types in powerful ways, enabling you to create subsets, exclude properties, and map keys to specific values dynamically. By understanding when and how to use these utility types, you can write more flexible and maintainable code that aligns with TypeScript’s static typing system.

These tools are essential for working with complex data structures, ensuring that your types are both accurate and flexible enough to meet your application’s needs.

Utility Types Part 1: Partial, Required, Readonly

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are Utility Types in TypeScript?
  • Overview of Partial, Required, and Readonly Utility Types
  • Partial<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Required<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Readonly<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Practical Examples
    • Example 1: Making Properties Optional with Partial
    • Example 2: Ensuring All Properties Are Required with Required
    • Example 3: Protecting Object Properties with Readonly
  • When and Why to Use Utility Types
  • Conclusion

Introduction

TypeScript provides a set of built-in utility types that allow developers to modify types in various ways. These utility types make it easier to work with types in a flexible yet type-safe manner, without manually creating type transformations. In this article, we will dive into the first part of TypeScript’s utility types: Partial, Required, and Readonly. Each of these utility types provides a powerful tool for adjusting properties in types, which can be especially helpful for working with objects and ensuring type safety across your application.


What Are Utility Types in TypeScript?

Utility types are predefined types that can be applied to other types to modify their properties or behavior. They are part of TypeScript’s standard library and enable developers to quickly adapt types to different needs, reducing boilerplate code and increasing productivity.

In this article, we’ll focus on three important utility types:

  • Partial<T>: Makes all properties of a type optional.
  • Required<T>: Makes all properties of a type required.
  • Readonly<T>: Makes all properties of a type read-only.

These utility types work with any type, and they can be particularly useful for transforming and manipulating complex objects.


Overview of Partial, Required, and Readonly Utility Types

Before we dive deeper into each type, let’s have an overview of how these utility types behave:

  1. Partial<T>: Converts all properties in the given type T to be optional.
  2. Required<T>: Converts all properties in the given type T to be required, even if they were previously optional.
  3. Readonly<T>: Converts all properties in the given type T to be read-only, meaning they cannot be reassigned after initialization.

Now, let’s explore each utility type in detail.


Partial<T>

Definition and Syntax

The Partial<T> utility type constructs a type that has all properties of type T set to optional. This is useful when you want to represent a partial version of an object type, such as when updating an object incrementally or performing operations where not all properties are required.

The syntax for Partial is as follows:

type Partial<T> = {
[P in keyof T]?: T[P];
};

In this definition:

  • T is the original type.
  • keyof T is a union of all property names in the type T.
  • T[P] refers to the type of each property in T.
  • The ? makes each property optional.

Use Cases and Examples

  • Updating objects incrementally: When working with objects that may not have all properties defined during an update.
  • Handling API responses: Sometimes, you may receive partial data from an API, where not all properties are available.

Example:

interface User {
id: number;
name: string;
email: string;
}

const updateUser = (user: User, updatedFields: Partial<User>) => {
return { ...user, ...updatedFields };
};

const existingUser: User = { id: 1, name: 'Alice', email: '[email protected]' };

const updatedUser = updateUser(existingUser, { name: 'Alice Updated' });

console.log(updatedUser); // { id: 1, name: 'Alice Updated', email: '[email protected]' }

In this example, the updatedFields parameter is of type Partial<User>, which means only the properties provided are updated, and all other properties remain unchanged.


Required<T>

Definition and Syntax

The Required<T> utility type makes all properties in the given type T required, even if they were previously optional. This is particularly useful when you want to ensure that all properties in an object are defined, such as when performing validation or creating an object that needs to have all fields filled out.

The syntax for Required is:

type Required<T> = {
[P in keyof T]-?: T[P];
};

In this definition:

  • T is the original type.
  • keyof T is the union of property names.
  • T[P] is the type of each property.
  • The -? removes the optional modifier, making each property required.

Use Cases and Examples

  • Ensuring all fields are provided: In situations where you want to ensure an object is fully defined before being used.
  • Validating complete data: For validation, you may want to force all properties of an object to be required.

Example:

interface Product {
name?: string;
price?: number;
}

const createProduct = (product: Required<Product>) => {
console.log(`Product name: ${product.name}, Product price: ${product.price}`);
};

createProduct({ name: 'Laptop', price: 1200 }); // Works fine
createProduct({}); // Error: Property 'name' is missing in type '{}' but required in type 'Required<Product>'.

In this example, the Required<Product> type ensures that both name and price are required properties, and passing an object without them will result in a compilation error.


Readonly<T>

Definition and Syntax

The Readonly<T> utility type makes all properties in the given type T read-only. This means you cannot modify the properties after the object has been created, providing immutability.

The syntax for Readonly is:

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

In this definition:

  • T is the original type.
  • keyof T is the union of property names.
  • T[P] is the type of each property.
  • The readonly modifier ensures that the property is immutable.

Use Cases and Examples

  • Ensuring immutability: When you want to protect object properties from being modified.
  • Defining constants: For defining configuration objects or constants that should not be modified after creation.

Example:

interface Config {
readonly apiUrl: string;
readonly timeout: number;
}

const config: Readonly<Config> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};

config.apiUrl = 'https://api.newurl.com'; // Error: Cannot assign to 'apiUrl' because it is a read-only property.

In this example, the Readonly<Config> type ensures that both apiUrl and timeout cannot be modified after the object is created. Any attempt to change their values results in an error.


Practical Examples

Example 1: Making Properties Optional with Partial

You can use Partial when updating a user profile:

interface UserProfile {
name: string;
age: number;
address: string;
}

const updateProfile = (profile: UserProfile, updatedData: Partial<UserProfile>) => {
return { ...profile, ...updatedData };
};

This allows you to update only a subset of the UserProfile properties.

Example 2: Ensuring All Properties Are Required with Required

For a product creation process, you might want to ensure that all fields are provided:

interface Product {
name?: string;
price?: number;
}

const createProduct = (product: Required<Product>) => {
console.log(product);
};

createProduct({ name: 'TV', price: 1000 }); // Works
createProduct({}); // Error: Property 'name' is required.

Example 3: Protecting Object Properties with Readonly

You can use Readonly to prevent modification of critical configuration settings:

interface Config {
readonly appName: string;
readonly version: string;
}

const config: Readonly<Config> = { appName: 'MyApp', version: '1.0' };
config.version = '1.1'; // Error: Cannot assign to 'version' because it is a read-only property.

When and Why to Use Utility Types

  1. Partial: Use Partial when you need to make a subset of object properties optional. This is useful in scenarios where you only need to update part of an object or work with incomplete data.
  2. Required: Use Required when you want to enforce that all properties of an object are present. This is useful in validation or when dealing with objects where you cannot afford any undefined or missing properties.
  3. Readonly: Use Readonly when you need to make an object immutable. This is particularly useful for configuration objects, constants, or data that should not be changed after initialization.

Conclusion

In this first part of our exploration into TypeScript’s utility types, we’ve learned about Partial, Required, and Readonly. These utility types provide a flexible and powerful way to modify existing types, ensuring better type safety and control over your objects. Whether you need to update objects incrementally, ensure all fields are required, or enforce immutability, these utility types can significantly enhance your development experience.

By leveraging utility types, you can write cleaner and more maintainable code while maintaining the type safety that TypeScript provides.