Advanced Types in TypeScript

Learn about Advanced Types in TypeScript with this detailed tutorial. Includes explanations and examples to help you master advanced type features.

Introduction

TypeScript's advanced types allow you to create powerful type definitions that can help you write more robust and maintainable code. In this tutorial, we will cover some of the most important advanced types and their use cases.

Intersection Types

Intersection types allow you to combine multiple types into one. This can be useful when you want to merge the properties of multiple types.

interface Person {
    name: string;
}

interface Employee {
    employeeId: number;
}

type PersonEmployee = Person & Employee;

const pe: PersonEmployee = {
    name: "John",
    employeeId: 1234
};

Explanation

In this example, the PersonEmployee type is created by combining the Person and Employee interfaces using the & operator. The resulting type has all the properties of both interfaces.

Union Types

Union types allow you to define a variable that can hold multiple types. This can be useful when a value can be one of several types.

function printId(id: number | string) {
    console.log(`Your ID is: ${id}`);
}

printId(101); // Your ID is: 101
printId("202"); // Your ID is: 202

Explanation

In this example, the printId function accepts a parameter that can be either a number or a string. The function can handle both types of inputs.

Type Guards

Type guards allow you to narrow down the type of a variable within a conditional block. This can be useful when working with union types.

function isString(x: any): x is string {
    return typeof x === "string";
}

function printValue(value: number | string) {
    if (isString(value)) {
        console.log(`String: ${value}`);
    } else {
        console.log(`Number: ${value}`);
    }
}

printValue("Hello"); // String: Hello
printValue(123); // Number: 123

Explanation

In this example, the isString function is a type guard that checks if a value is a string. The printValue function uses this type guard to determine the type of the input and handle it accordingly.

Type Aliases

Type aliases allow you to create new names for existing types. This can be useful for simplifying complex type definitions.

type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
    console.log(`Your ID is: ${id}`);
}

printId(101); // Your ID is: 101
printId("202"); // Your ID is: 202

Explanation

In this example, the StringOrNumber type alias is used to create a shorthand for the union type string | number. The printId function uses this type alias to specify its parameter type.

Index Types

Index types allow you to get the type of a property from another type. This can be useful for working with dynamic property names.

interface Person {
    name: string;
    age: number;
}

type PersonKeys = keyof Person; // "name" | "age"

function getProperty(obj: T, key: K) {
    return obj[key];
}

let person: Person = { name: "John", age: 30 };
let personName = getProperty(person, "name"); // John

Explanation

In this example, the keyof operator is used to get the keys of the Person interface. The getProperty function uses this type to ensure that only valid keys are accessed on the object.

Mapped Types

Mapped types allow you to create new types by transforming properties of an existing type. This can be useful for creating variations of a type.

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

interface Person {
    name: string;
    age: number;
}

let readonlyPerson: Readonly = { name: "John", age: 30 };
// readonlyPerson.name = "Doe"; // Error: Cannot assign to 'name' because it is a read-only property.

Explanation

In this example, the Readonly mapped type makes all properties of the Person interface read-only. This ensures that the properties cannot be modified after the object is created.

Conditional Types

Conditional types allow you to create types based on a condition. This can be useful for creating flexible and reusable types.

type IsString = T extends string ? true : false;

type A = IsString; // true
type B = IsString; // false

Explanation

In this example, the IsString conditional type checks if a type is a string. The resulting type is either true or false based on the condition.

Utility Types

TypeScript provides several utility types that make it easier to work with advanced types. Some common utility types include Partial, Required, Pick, and Omit.

Partial

The Partial utility type makes all properties of a type optional.

interface Person {
    name: string;
    age: number;
}

let partialPerson: Partial = {};
partialPerson.name = "John"; // OK
partialPerson.age = 30; // OK

Required

The Required utility type makes all properties of a type required.

interface Person {
    name?: string;
    age?: number;
}

let requiredPerson: Required = {
    name: "John",
    age: 30
}; // OK

Pick

The Pick utility type allows you to create a new type by picking specific properties from an existing type.

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

type PersonName = Pick;

let personName: PersonName = {
    name: "John"
}; // OK

Omit

The Omit utility type allows you to create a new type by omitting specific properties from an existing type.

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

type PersonWithoutAddress = Omit;

let personWithoutAddress: PersonWithoutAddress = {
    name: "John",
    age: 30
}; // OK

Conclusion

Advanced types in TypeScript provide powerful tools for creating flexible, reusable, and type-safe code. By understanding and using these advanced types, you can write more robust and maintainable applications.