Features Examples Playground Articles

Master Advanced TypeScript Features

Explore advanced TypeScript features with interactive examples, AI assistance, and a professional code editor.

Advanced TypeScript Features

Union Types

Combine multiple types into one, allowing variables to accept different types of values.

Type Guards

Narrow down types with conditional checks, ensuring type safety at runtime.

Optional Chaining

Safely access nested properties without worrying about null or undefined values.

Generics

Create reusable components that work with multiple types while maintaining type information.

Type Assertions

Tell the compiler to treat a value as a specific type, useful when you know more about the type.

Type Predicates

Create functions that perform runtime checks and narrow types in conditional blocks.

Decorators

Add annotations and meta-programming syntax for classes, methods, and properties.

Advanced Types

Master mapped types, conditional types, and template literal types for advanced type manipulation.

Modules & Namespaces

Organize code with modules and namespaces for better structure and maintainability.

Utility Types

Use built-in utility types like Partial, Required, Pick, and Omit for common type transformations.

Type Inference

Understand how TypeScript infers types and how to work with contextual typing.

Declaration Files

Create and use declaration files to add type information to JavaScript libraries.

TypeScript Articles

Understanding Union Types in TypeScript

Union types are a powerful way to express a value that can be one of several types. You define a union type using the pipe character (|) between types.

// Defining a union type
type ID = string | number;

// Function that accepts a union type
function printID(id: ID) {
    if (typeof id === "string") {
        console.log(id.toUpperCase());
    } else {
        console.log(id.toFixed(2));
    }
}

printID("abc123"); // Output: ABC123
printID(123.456);  // Output: 123.46

// More complex union type
type Status = "pending" | "in-progress" | "completed" | "failed";

function handleStatus(status: Status) {
    switch(status) {
        case "pending":
            return "Task is waiting to start";
        case "in-progress":
            return "Task is currently running";
        case "completed":
            return "Task finished successfully";
        case "failed":
            return "Task encountered an error";
    }
}

console.log(handleStatus("in-progress")); // Output: Task is currently running

Union types are particularly useful when working with values that might come in different forms, such as API responses where a field could be a string, number, or null. They allow TypeScript to provide better type checking and enable autocompletion based on the possible types.

Type Guards for Runtime Safety

Type guards are expressions that perform runtime checks to guarantee the type of a value within a specific scope. They help you write type-safe code even when dealing with dynamic data.

// Type guard using typeof
function isString(value: any): value is string {
    return typeof value === "string";
}

// Type guard using instance checks
function isDate(value: any): value is Date {
    return value instanceof Date;
}

// Custom type guard
interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function isBird(pet: Bird | Fish): pet is Bird {
    return (pet as Bird).fly !== undefined;
}

function move(pet: Bird | Fish) {
    if (isBird(pet)) {
        pet.fly();
    } else {
        pet.swim();
    }
}

// Using "in" operator for type guards
function calculateArea(shape: Circle | Square): number {
    if ('radius' in shape) {
        // TypeScript knows shape is Circle here
        return Math.PI * shape.radius ** 2;
    } else {
        // TypeScript knows shape is Square here
        return shape.sideLength ** 2;
    }
}

// User-defined type guards with validation
function isValidEmail(value: any): value is string {
    return typeof value === "string" && 
           value.includes("@") && 
           value.length > 5;
}

const userInput = "test@example.com";
if (isValidEmail(userInput)) {
    // TypeScript knows userInput is a valid email string
    console.log(`Sending email to ${userInput}`);
}

Type guards are essential for working with union types and ensuring your code is both flexible and type-safe. They allow TypeScript's compiler to narrow down types based on conditional checks, providing better autocompletion and error detection.

Optional Chaining and Nullish Coalescing

Optional chaining (?.) and nullish coalescing (??) are modern JavaScript features that TypeScript fully supports. They make working with nested properties and default values much cleaner.

interface User {
    name: string;
    address?: {
        street: string;
        city: string;
        zipcode?: string;
    };
    preferences?: {
        notifications?: {
            email?: boolean;
            sms?: boolean;
        }
    }
}

const user: User = {
    name: "John Doe",
    // address is optional and might be undefined
};

// Without optional chaining
const city = user.address && user.address.city;

// With optional chaining (much cleaner!)
const cityWithChaining = user.address?.city;

// Nullish coalescing for default values
const zipcode = user.address?.zipcode ?? "00000";

console.log(city); // Output: undefined
console.log(cityWithChaining); // Output: undefined
console.log(zipcode); // Output: 00000

// Deep optional chaining
const wantsEmailNotifications = user.preferences?.notifications?.email ?? true;

// Optional chaining with function calls
const someObject: { method?: () => string } = {};
const result = someObject.method?.(); // Returns undefined if method doesn't exist

// Combining with nullish coalescing
const output = someObject.method?.() ?? "default value";

// Optional element access
const firstItem = someArray?.[0];

// Practical example with API responses
interface ApiResponse {
    data?: {
        user?: {
            name: string;
            age: number;
            friends?: string[];
        }
    }
}

function processApiResponse(response: ApiResponse) {
    const userName = response.data?.user?.name ?? "Unknown User";
    const friendCount = response.data?.user?.friends?.length ?? 0;
    
    console.log(`${userName} has ${friendCount} friends`);
}

These features help eliminate verbose null/undefined checks and make your code more readable and maintainable. Optional chaining short-circuits and returns undefined if any part of the chain is null or undefined, while nullish coalescing provides default values only when the left-hand side is null or undefined.

Understanding Generics in TypeScript

Generics allow you to create reusable components that can work with multiple types while maintaining type information. They provide a way to create functions, classes, and interfaces that can work with any type, but with type safety.

// Basic generic function
function identity(arg: T): T {
    return arg;
}

let output1 = identity("myString"); // type of output will be 'string'
let output2 = identity(100); // type of output will be 'number'

// Generic interfaces
interface GenericIdentityFn {
    (arg: T): T;
}

function identity(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

// Generic classes
class GenericNumber {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

// Generic constraints
interface Lengthwise {
    length: number;
}

function loggingIdentity(arg: T): T {
    console.log(arg.length); // Now we know it has a .length property
    return arg;
}

// Using type parameters in generic constraints
function getProperty(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error

// Generic utility types
interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

// Partial makes all properties optional
function updateTodo(todo: Todo, fieldsToUpdate: Partial) {
    return { ...todo, ...fieldsToUpdate };
}

// Readonly makes all properties readonly
const todo: Readonly = {
    title: "Delete inactive users",
    description: "...",
    completed: false
};

// Record creates a type with specified keys and value type
const pages: Record = {
    home: { title: "Home" },
    about: { title: "About" },
    contact: { title: "Contact" }
};

Generics are one of the most powerful features in TypeScript. They enable you to create flexible and reusable code while maintaining full type safety. By using generics, you can avoid the pitfalls of the "any" type while still writing code that works with multiple data types.

Type Assertions in TypeScript

Type assertions are a way to tell the compiler "trust me, I know what I'm doing." They are like type casts in other languages but perform no special checking or restructuring of data. They have no runtime impact and are used purely by the TypeScript compiler.

// Basic type assertion
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

// Alternative syntax (angle-bracket)
let strLength2: number = (someValue).length;

// Type assertion with objects
interface Employee {
    name: string;
    code: number;
}

let employee = {} as Employee;
employee.name = "John"; // OK
employee.code = 123; // OK

// Asserting with complex types
let someArray: any[] = [1, true, "free"];
let firstElement = someArray[0] as number;

// Double assertions
let someValue2: unknown = "hello world";
let strLength3: number = (someValue2 as string).length;

// Const assertions
let x = "hello" as const; // type is "hello" (not string)
let y = [10, 20] as const; // type is readonly [10, 20]
let z = { text: "hello" } as const; // type is { readonly text: "hello" }

// Type assertion functions
function assertIsNumber(val: any): asserts val is number {
    if (typeof val !== "number") {
        throw new Error("Not a number!");
    }
}

function multiply(x: any, y: any) {
    assertIsNumber(x);
    assertIsNumber(y);
    // Now TypeScript knows x and y are numbers
    return x * y;
}

// When to use type assertions
// 1. When you know more about the type than TypeScript does
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;

// 2. When migrating from JavaScript to TypeScript
const person = {} as Person;
person.name = "Alice";
person.age = 30;

// 3. When working with union types
let padding: string | number;
// Later in code...
let paddingPx = (padding as number) + "px";

// 4. With incomplete initialization
interface ApiResponse {
    data: {
        user: {
            id: number;
            name: string;
        }
    }
}

// We know the response will have this structure
const response = {} as ApiResponse;

// Dangerous examples (avoid when possible)
const value = "hello" as any as number; // Double assertion to force type
const element = document.getElementById("root") as any as HTMLElement;

Type assertions should be used sparingly and only when you have more information about the type than TypeScript can infer. While they can be useful, overusing type assertions can defeat the purpose of using TypeScript in the first place, as they essentially tell the compiler to skip type checking.

Type Predicates in TypeScript

Type predicates are special functions that return a boolean and have a type predicate as their return type. They are used to narrow down types within conditional blocks and are more powerful than simple boolean checks.

// Basic type predicate
function isString(test: any): test is string {
    return typeof test === "string";
}

function example(foo: any) {
    if (isString(foo)) {
        // foo is type string in this block
        console.log(foo.length);
    }
}

// Type predicate with interfaces
interface Cat {
    meow(): void;
}

interface Dog {
    bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function letAnimalSpeak(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();
    } else {
        animal.bark();
    }
}

// Type predicates with classes
class Success {
    constructor(public data: string[]) {}
}

class Failure {
    constructor(public error: string) {}
}

function isSuccess(result: Success | Failure): result is Success {
    return result instanceof Success;
}

function handleResult(result: Success | Failure) {
    if (isSuccess(result)) {
        console.log(result.data.join(", "));
    } else {
        console.error(result.error);
    }
}

// Type predicates with discriminated unions
type Circle = {
    kind: "circle";
    radius: number;
}

type Square = {
    kind: "square";
    sideLength: number;
}

type Shape = Circle | Square;

function isCircle(shape: Shape): shape is Circle {
    return shape.kind === "circle";
}

function calculateArea(shape: Shape): number {
    if (isCircle(shape)) {
        return Math.PI * shape.radius ** 2;
    } else {
        return shape.sideLength ** 2;
    }
}

// Type predicates with type guards in classes
class Car {
    drive() {
        console.log("Driving a car");
    }
}

class Boat {
    sail() {
        console.log("Sailing a boat");
    }
}

function isCar(vehicle: Car | Boat): vehicle is Car {
    return vehicle instanceof Car;
}

function operateVehicle(vehicle: Car | Boat) {
    if (isCar(vehicle)) {
        vehicle.drive();
    } else {
        vehicle.sail();
    }
}

// Complex type predicate with validation
interface User {
    id: number;
    name: string;
    email: string;
}

function isValidUser(obj: any): obj is User {
    return obj &&
           typeof obj.id === "number" &&
           typeof obj.name === "string" &&
           typeof obj.email === "string" &&
           obj.email.includes("@");
}

// Usage with API responses
async function fetchUser(id: number): Promise {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();
    
    if (isValidUser(data)) {
        return data;
    } else {
        throw new Error("Invalid user data received");
    }
}

// Type predicates with generic functions
function isOfType(obj: any, key: keyof T): obj is T {
    return obj && typeof obj === "object" && key in obj;
}

function processData(data: unknown) {
    if (isOfType<{ value: number }>(data, "value")) {
        // data is now { value: number }
        console.log(data.value.toFixed(2));
    }
}

// Type predicates with multiple conditions
function isAdmin(user: User): user is User & { role: "admin" } {
    return "role" in user && user.role === "admin";
}

function handleUser(user: User) {
    if (isAdmin(user)) {
        // user has role admin here
        console.log(`Admin user: ${user.name}`);
    } else {
        console.log(`Regular user: ${user.name}`);
    }
}

Type predicates are an advanced TypeScript feature that allows you to create custom type guards with explicit type information. They are extremely useful for validating data from external sources (like APIs) and for creating robust, type-safe conditional logic in your applications. By using type predicates, you can move type checking logic into reusable functions that improve both code readability and type safety.

Decorators in TypeScript

Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. They are a stage 2 proposal for JavaScript and are available as an experimental feature in TypeScript.

// Class decorator
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

// Method decorator
function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

// Accessor decorator
function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

// Property decorator
function format(formatString: string) {
    return function (target: any, propertyKey: string): any {
        let value = target[propertyKey];

        function getter() {
            return `${formatString} ${value}`;
        }

        function setter(newVal: string) {
            value = newVal;
        }

        return {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true,
        };
    }
}

class Greeter {
    @format('Hello')
    greeting: string;
}

// Parameter decorator
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    console.log(`Parameter ${parameterIndex} in ${String(propertyKey)} is required`);
}

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

// Decorator factory
function color(value: string) {
    return function (target: any) {
        // do something with 'target' and 'value'...
    };
}

// Decorator composition
@first()
@second()
class ExampleClass { }

function first() {
    console.log("first(): factory evaluated");
    return function (target: any) {
        console.log("first(): called");
    };
}

function second() {
    console.log("second(): factory evaluated");
    return function (target: any) {
        console.log("second(): called");
    };
}

// Practical example: Logging decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
        console.log(`Calling ${key} with args: ${JSON.stringify(args)}`);
        const result = originalMethod.apply(this, args);
        console.log(`${key} returned: ${JSON.stringify(result)}`);
        return result;
    };
    
    return descriptor;
}

class Calculator {
    @log
    add(x: number, y: number): number {
        return x + y;
    }
}

const calc = new Calculator();
calc.add(5, 3);

Decorators are a powerful meta-programming feature that can be used to modify or annotate classes and class members. They are widely used in frameworks like Angular to provide functionality like dependency injection, component definitions, and more. While still experimental in TypeScript, they offer significant capabilities for creating clean, declarative code.

Advanced Types in TypeScript

TypeScript offers several advanced type manipulation features including Mapped Types, Conditional Types, and Template Literal Types that allow for sophisticated type transformations and operations.

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

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

type Nullable = {
    [P in keyof T]: T[P] | null;
}

// Practical example with mapped types
interface Person {
    name: string;
    age: number;
    email: string;
}

type ReadonlyPerson = Readonly;
type PartialPerson = Partial;
type NullablePerson = Nullable;

// Conditional Types
type TypeName =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName;  // "string"
type T1 = TypeName<"a">;     // "string"
type T2 = TypeName;    // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName;    // "object"

// Infer keyword in conditional types
type ReturnType = T extends (...args: any[]) => infer R ? R : any;

type T5 = ReturnType<() => string>;        // string
type T6 = ReturnType<(s: string) => void>; // void
type T7 = ReturnType<() => T>;          // {}
type T8 = ReturnType;  // number

// Template Literal Types
type World = "world";
type Greeting = `hello ${World}`; // "hello world"

type Color = "red" | "blue";
type Quantity = "primary" | "secondary";

type ColorQuantity = `${Color}-${Quantity}`;
// "red-primary" | "red-secondary" | "blue-primary" | "blue-secondary"

// Advanced mapped types with template literals
type Getters = {
    [K in keyof T as `get${Capitalize}`]: () => T[K]
};

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

type LazyPerson = Getters;
// {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }

// Utility types deep dive
type Required = {
    [P in keyof T]-?: T[P];
};

type Pick = {
    [P in K]: T[P];
};

type Record = {
    [P in K]: T;
};

type Exclude = T extends U ? never : T;

type Extract = T extends U ? T : never;

type Omit = Pick>;

// Practical examples with utility types
interface Todo {
    title: string;
    description: string;
    completed: boolean;
    createdAt: number;
}

// Create a type with all properties optional
type PartialTodo = Partial;

// Create a type with only title and completed
type TodoPreview = Pick;

// Create a type that omits description
type TodoWithoutDescription = Omit;

// Create a mapped type that adds metadata
type WithMetadata = {
    value: T;
    timestamp: Date;
    version: number;
};

type TodoWithMetadata = WithMetadata;

// Recursive types
type Json =
    | string
    | number
    | boolean
    | null
    | { [property: string]: Json }
    | Json[];

const jsonData: Json = {
    name: "John",
    age: 30,
    hobbies: ["reading", "swimming"],
    address: {
        street: "123 Main St",
        city: "Anytown"
    }
};

// Conditional type with union distribution
type ToArray = T extends any ? T[] : never;
type StrOrNumArray = ToArray; // string[] | number[]

// Non-distributive conditional type
type ToArrayNonDist = [T] extends [any] ? T[] : never;
type StrOrNumArrayNonDist = ToArrayNonDist; // (string | number)[]

// Complex conditional type example
type DeepReadonly = 
    T extends Function ? T :
    T extends object ? { readonly [K in keyof T]: DeepReadonly } :
    T;

interface Example {
    a: number;
    b: {
        c: string;
        d: {
            e: boolean;
        };
    };
}

type ReadonlyExample = DeepReadonly;

Advanced types in TypeScript provide powerful tools for creating flexible, type-safe abstractions. Mapped types allow you to create new types based on existing ones, conditional types enable type-level logic, and template literal types facilitate string pattern matching. These features are particularly useful for library authors and developers working on large codebases where type safety and maintainability are critical.

Modules and Namespaces in TypeScript

TypeScript provides two systems for organizing code: modules and namespaces. Modules are the recommended approach for modern applications, while namespaces are useful for organizing code in a global namespace.

// Module example (ES6 modules)
// math.ts
export function add(x: number, y: number): number {
    return x + y;
}

export function subtract(x: number, y: number): number {
    return x - y;
}

// app.ts
import { add, subtract } from './math';
console.log(add(5, 3)); // Output: 8

// Namespace example
namespace Geometry {
    export interface Point {
        x: number;
        y: number;
    }
    
    export function distance(p1: Point, p2: Point): number {
        return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
    }
}

// Using the namespace
const point1: Geometry.Point = { x: 0, y: 0 };
const point2: Geometry.Point = { x: 3, y: 4 };
console.log(Geometry.distance(point1, point2)); // Output: 5

// Module vs Namespace
// Modules:
// - Use file-based organization
// - Use import/export syntax
// - Better for large applications
// - Supported by modern bundlers

// Namespaces:
// - Use logical grouping within a file
// - Use namespace keyword and dot notation
// - Can be split across files using reference comments
// - Useful for organizing code in a global context

// Dynamic imports
async function loadMathModule() {
    const math = await import('./math');
    console.log(math.add(10, 5)); // Output: 15
}

// Re-exporting
// shapes.ts
export class Circle { /* ... */ }
export class Square { /* ... */ }

// index.ts
export * from './shapes'; // Re-export all from shapes

// Default exports
// logger.ts
export default class Logger {
    log(message: string) {
        console.log(message);
    }
}

// app.ts
import Logger from './logger';
const logger = new Logger();
logger.log('Hello world');

// Namespace with multiple files
// geometry.ts
namespace Geometry {
    export interface Point {
        x: number;
        y: number;
    }
}

// circle.ts
/// 
namespace Geometry {
    export class Circle {
        constructor(public center: Point, public radius: number) {}
        
        area(): number {
            return Math.PI * this.radius ** 2;
        }
    }
}

// app.ts
/// 
/// 

const circle = new Geometry.Circle({ x: 0, y: 0 }, 5);
console.log(circle.area()); // Output: ~78.54

// Module resolution strategies
// Classic: TypeScript's former default resolution strategy
// Node: Mimics Node.js module resolution

// Path mapping in tsconfig.json
// {
//   "compilerOptions": {
//     "baseUrl": "./",
//     "paths": {
//       "@utils/*": ["src/utils/*"],
//       "@models": ["src/models/index"]
//     }
//   }
// }

// Using path mapping
import { formatDate } from '@utils/dateFormatter';
import { User } from '@models';

// Ambient modules
// Declare modules for libraries without type definitions
declare module "my-library" {
    export function doSomething(): void;
    export const importantValue: number;
}

// Using the declared module
import { doSomething, importantValue } from "my-library";

// Module augmentation
// Adding functionality to existing modules
import { Observable } from "rxjs";

declare module "rxjs" {
    interface Observable {
        mapToUpperCase(): Observable;
    }
}

Observable.prototype.mapToUpperCase = function () {
    return this.map((value: string) => value.toUpperCase());
};

// Using the augmented module
const source = new Observable(subscriber => {
    subscriber.next('hello');
    subscriber.next('world');
});

source.mapToUpperCase().subscribe(value => console.log(value));
// Output: HELLO, WORLD

// ES modules vs CommonJS
// ES modules (import/export) - modern standard
// CommonJS (require/module.exports) - Node.js style

// Using CommonJS in TypeScript
import fs = require('fs');
// or
import * as fs from 'fs';

// Export equals
// my-module.ts
class MyClass {
    // ...
}
export = MyClass;

// app.ts
import MyClass = require('./my-module');

// Working with JSON modules
// With "resolveJsonModule": true in tsconfig.json
import data from './data.json';
console.log(data.property);

// Wildcard module declarations
declare module "*.json" {
    const value: any;
    export default value;
}

declare module "*.css" {
    const content: { [className: string]: string };
    export default content;
}

// Using CSS modules
import styles from './App.css';
const className = styles.myClass;

// Module best practices
// 1. Use ES modules for new code
// 2. Prefer named exports over default exports
// 3. Use index.ts files for clean imports
// 4. Use path mapping for avoiding relative path hell
// 5. Use ambient modules for library type definitions

Understanding modules and namespaces is crucial for organizing TypeScript code effectively. While modules are the standard for modern applications, namespaces can still be useful in certain scenarios, particularly when working with legacy code or when you need to avoid polluting the global namespace. TypeScript's flexible module system supports various module formats and provides powerful features like path mapping and module augmentation to help create maintainable, well-organized codebases.

Utility Types in TypeScript

TypeScript provides several built-in utility types that facilitate common type transformations. These utilities are available globally and can be extremely helpful for everyday TypeScript development.

// Partial - Make all properties in T optional
interface Todo {
    title: string;
    description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial) {
    return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
    title: "Learn TypeScript",
    description: "Study utility types"
};

const todo2 = updateTodo(todo1, {
    description: "Study advanced TypeScript features"
});

// Required - Make all properties in T required
interface Props {
    a?: number;
    b?: string;
}

const obj: Props = { a: 5 }; // OK
const obj2: Required = { a: 5, b: "hello" }; // Error: Property 'b' is missing

// Readonly - Make all properties in T readonly
interface Todo {
    title: string;
}

const todo: Readonly = {
    title: "Delete inactive users"
};

todo.title = "Hello"; // Error: cannot assign to readonly property

// Record - Construct a type with a set of properties K of type T
interface CatInfo {
    age: number;
    breed: string;
}

type CatName = "miffy" | "boris" | "mordred";

const cats: Record = {
    miffy: { age: 10, breed: "Persian" },
    boris: { age: 5, breed: "Maine Coon" },
    mordred: { age: 16, breed: "British Shorthair" }
};

// Pick - From T, pick a set of properties whose keys are in the union K
interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Pick;

const todo: TodoPreview = {
    title: "Clean room",
    completed: false
};

// Omit - From T, remove a set of properties whose keys are in the union K
interface Todo {
    title: string;
    description: string;
    completed: boolean;
    createdAt: number;
}

type TodoPreview = Omit;

const todo: TodoPreview = {
    title: "Clean room",
    completed: false,
    createdAt: 1615544252770
};

type TodoInfo = Omit;

const todoInfo: TodoInfo = {
    title: "Pick up kids",
    description: "Kindergarten closes at 5pm"
};

// Exclude - Exclude from T those types that are assignable to U
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude void), Function>; // string | number

// Extract - Extract from T those types that are assignable to U
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract void), Function>; // () => void

// NonNullable - Exclude null and undefined from T
type T0 = NonNullable; // string | number
type T1 = NonNullable; // string[]

// Parameters - Obtain the parameters of a function type in a tuple
declare function f1(arg: { a: number; b: string }): void;

type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [string]
type T2 = Parameters<(arg: T) => T>; // [unknown]
type T3 = Parameters; // [{ a: number; b: string }]

// ReturnType - Obtain the return type of a function type
declare function f1(): { a: number; b: string };

type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<() => T>; // unknown
type T3 = ReturnType<() => T>; // number[]
type T4 = ReturnType; // { a: number; b: string }

// InstanceType - Obtain the instance type of a constructor function type
class C {
    x = 0;
    y = 0;
}

type T0 = InstanceType; // C
type T1 = InstanceType; // any
type T2 = InstanceType; // never

// ThisType - Marker for contextual 'this' type
interface ObjectDescriptor {
    data?: D;
    methods?: M & ThisType; // Type of 'this' in methods is D & M
}

function makeObject(desc: ObjectDescriptor): D & M {
    let data: object = desc.data || {};
    let methods: object = desc.methods || {};
    return { ...data, ...methods } as D & M;
}

let obj = makeObject({
    data: { x: 0, y: 0 },
    methods: {
        moveBy(dx: number, dy: number) {
            this.x += dx; // Strongly typed this
            this.y += dy; // Strongly typed this
        }
    }
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Greeting = "Hello, world";
type ShoutyGreeting = Uppercase; // "HELLO, WORLD"

type ASCIICacheKey = `ID-${Uppercase}`;
type MainID = ASCIICacheKey<"my_app">; // "ID-MY_APP"

type QuietGreeting = Lowercase; // "hello, world"

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize; // "Hello, world"

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize; // "hELLO WORLD"

Utility types are one of TypeScript's most powerful features, allowing developers to perform complex type transformations with minimal code. They're particularly useful for creating flexible APIs, managing state shapes, and ensuring type safety across large codebases. By mastering these utility types, you can write more maintainable and robust TypeScript code.

Type Inference in TypeScript

TypeScript's type inference system automatically determines types when they're not explicitly annotated. Understanding how type inference works can help you write cleaner code while maintaining type safety.

// Basic type inference
let x = 3; // TypeScript infers type 'number'
let y = "hello"; // TypeScript infers type 'string'
let z = [0, 1, null]; // TypeScript infers type '(number | null)[]'

// Contextual typing
window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.button); // OK
    console.log(mouseEvent.kangaroo); // Error: Property 'kangaroo' does not exist on type 'MouseEvent'
};

// Best common type
let zoo = [new Rhino(), new Elephant(), new Snake()];
// TypeScript infers type (Rhino | Elephant | Snake)[]

// Contextual typing also applies to return types
// The return type of the following function is inferred as number
function add(a: number, b: number) {
    return a + b;
}

// Object literal inference
const person = {
    name: "John",
    age: 30
}; // TypeScript infers { name: string; age: number; }

// Function type inference
const names = ["Alice", "Bob", "Eve"];

// Contextual typing applies to arrow function parameters
names.forEach(function(s) {
    console.log(s.toUpperCase()); // s is inferred as string
});

// Also applies to arrow functions
names.forEach(s => console.log(s.toUpperCase())); // s is inferred as string

// Return type inference based on usage
// The return type is inferred as number[]
function mapNumbers(numbers: number[]) {
    return numbers.map(x => x * 2);
}

// Type inference in generics
function identity(arg: T): T {
    return arg;
}

let output = identity("myString"); // type of output is string - T is inferred as string

// Multiple type arguments inference
function mapArray(arr: T[], fn: (x: T) => U): U[] {
    return arr.map(fn);
}

const lengths = mapArray(["hello", "world"], s => s.length); // T inferred as string, U inferred as number

// Const contexts
// Type is { name: string; age: number; }
const person1 = {
    name: "John",
    age: 30
};

// Type is { readonly name: "John"; readonly age: 30; }
const person2 = {
    name: "John",
    age: 30
} as const;

// Type inference with conditional types
type IsString = T extends string ? true : false;

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

// Inference in conditional types with infer keyword
type GetReturnType = T extends (...args: any[]) => infer R ? R : never;

type Num = GetReturnType<() => number>; // number
type Str = GetReturnType<(x: string) => string>; // string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]

// Inference in template literal types
type EventName = `${T}Changed`;
type Concat = `${S1}${S2}`;

type T0 = EventName<'foo'>; // 'fooChanged'
type T1 = Concat<'Hello', 'World'>; // 'HelloWorld'

// Type inference best practices
// 1. Let TypeScript infer types when possible
// Instead of:
const array: number[] = [1, 2, 3];
// Prefer:
const array = [1, 2, 3]; // TypeScript infers number[]

// 2. Use explicit types for public API boundaries
interface User {
    id: number;
    name: string;
}

function getUser(id: number): User {
    // function implementation
}

// 3. Use const assertions for literal values
// Instead of:
const colors = ['red', 'green', 'blue']; // string[]
// Prefer:
const colors = ['red', 'green', 'blue'] as const; // readonly ["red", "green", "blue"]

// 4. Understand when to use type annotations vs inference
// Use annotations for:
// - Function parameters
// - Object literals that need specific shapes
// - Variables that will be reassigned to different types

// Use inference for:
// - Most variable declarations
// - Function return types (unless complex)
// - Array and object literals

// Advanced inference example
type DeepPartial = T extends object ? {
    [P in keyof T]?: DeepPartial;
} : T;

interface Company {
    name: string;
    address: {
        street: string;
        city: string;
    };
}

const company: DeepPartial = {
    address: {
        city: "San Francisco"
    }
};

TypeScript's type inference is sophisticated and can handle most common programming patterns. By understanding how inference works, you can strike the right balance between explicit type annotations and letting TypeScript do the work for you. This leads to cleaner, more maintainable code without sacrificing type safety.

Working with Declaration Files

Declaration files (with .d.ts extension) allow you to describe the shape of existing JavaScript code, enabling TypeScript to provide type checking and editor support for JavaScript libraries.

// Basic declaration file structure
// types.d.ts
declare interface User {
    id: number;
    name: string;
    email: string;
}

declare function getUser(id: number): User;
declare function createUser(user: User): number;

// Global declarations
// globals.d.ts
declare namespace MyGlobal {
    function utilityFunction(): void;
    const importantValue: number;
}

// Using global declarations
MyGlobal.utilityFunction();
console.log(MyGlobal.importantValue);

// Module declarations for JavaScript libraries
// my-library.d.ts
declare module "my-library" {
    export function doSomething(): void;
    export const importantValue: number;
    export interface Options {
        timeout: number;
        retries: number;
    }
}

// Using the module declaration
import { doSomething, importantValue } from "my-library";

doSomething();
console.log(importantValue);

// Ambient module declarations for non-code resources
declare module "*.css" {
    const content: { [className: string]: string };
    export default content;
}

declare module "*.png" {
    const value: string;
    export default value;
}

// Using ambient modules
import styles from "./App.css";
import logo from "./logo.png";

const className = styles.myClass;
const imgSrc = logo;

// Declaring global variables
// globals.d.ts
declare const __VERSION__: string;
declare const __DEV__: boolean;

// Using global variables
console.log(`App version: ${__VERSION__}`);
if (__DEV__) {
    console.log("Running in development mode");
}

// Augmenting existing types
// augmentations.d.ts
declare global {
    interface Window {
        myCustomFunction: () => void;
        customProperty: string;
    }

    interface Date {
        format(pattern: string): string;
    }
}

// Using augmented types
window.myCustomFunction();
console.log(window.customProperty);

const today = new Date();
console.log(today.format("YYYY-MM-DD"));

// Module augmentation for existing modules
// observable.d.ts
import { Observable } from "./observable";

declare module "./observable" {
    interface Observable {
        map(f: (x: T) => U): Observable;
    }
}

// Using the augmented module
Observable.prototype.map = function (f) {
    // implementation
};

// Conditional exports in declaration files
// library.d.ts
declare module "library" {
    export function func1(): void;
    export function func2(): void;

    export namespace Advanced {
        export function advancedFunc(): void;
    }
}

// Using conditional exports
import { func1, Advanced } from "library";

func1();
Advanced.advancedFunc();

// Declaration merging
// interfaces.d.ts
interface User {
    id: number;
    name: string;
}

interface User {
    email: string;
    age?: number;
}

// The merged User interface has:
// id: number, name: string, email: string, age?: number

// Using declaration merging
const user: User = {
    id: 1,
    name: "John",
    email: "john@example.com"
};

// Namespace and function merging
function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
    export let suffix = "";
    export let prefix = "Hello, ";
}

console.log(buildLabel("John Smith")); // "Hello, John Smith"

// Class and namespace merging
class Album {
    label: Album.AlbumLabel;
}

namespace Album {
    export class AlbumLabel {}
}

// Creating declaration files for existing JavaScript
// jquery.d.ts
declare namespace JQuery {
    interface AjaxSettings {
        url: string;
        method?: string;
        data?: any;
    }

    function ajax(settings: AjaxSettings): void;
}

// Using the jQuery declarations
JQuery.ajax({
    url: "/api/data",
    method: "GET"
});

// Using triple-slash directives
/// 
/// 

import * as fs from "fs";

// The fs module now has type information from @types/node

// Writing declaration files for complex libraries
// complex-library.d.ts
declare namespace ComplexLibrary {
    interface Config {
        apiKey: string;
        endpoint: string;
        timeout?: number;
    }

    function initialize(config: Config): void;

    namespace Services {
        interface UserService {
            getUsers(): Promise;
            getUser(id: number): Promise;
        }

        function getUserService(): UserService;
    }

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

    type EventCallback = (data: any) => void;

    function on(event: string, callback: EventCallback): void;
    function off(event: string, callback: EventCallback): void;
}

// Using the complex library
ComplexLibrary.initialize({
    apiKey: "abc123",
    endpoint: "https://api.example.com"
});

const userService = ComplexLibrary.Services.getUserService();
userService.getUsers().then(users => {
    console.log(users);
});

// Best practices for declaration files
// 1. Use precise types instead of any when possible
// Instead of:
declare function processData(data: any): any;
// Prefer:
declare function processData(data: T): T;

// 2. Use union types for fixed sets of values
declare type Status = "pending" | "in-progress" | "completed" | "failed";

// 3. Use optional properties for optional parameters
interface Options {
    required: boolean;
    timeout?: number;
    retries?: number;
}

// 4. Use function overloads for different parameter patterns
declare function createElement(tag: "div"): HTMLDivElement;
declare function createElement(tag: "span"): HTMLSpanElement;
declare function createElement(tag: string): HTMLElement;

// 5. Use generics for reusable type patterns
declare interface ApiResponse {
    data: T;
    status: number;
    message: string;
}

// 6. Document your types with JSDoc comments
/**
 * Represents a user in the system
 * @property id - The unique identifier for the user
 * @property name - The user's full name
 * @property email - The user's email address
 */
interface User {
    id: number;
    name: string;
    email: string;
}

Declaration files are essential for using JavaScript libraries in TypeScript projects. They enable type safety, autocompletion, and documentation for existing JavaScript code. By mastering declaration files, you can improve the development experience for any library and ensure type safety across your entire codebase, even when working with untyped JavaScript.

TypeScript Examples

Union Types Example

Function that accepts both string and number types and returns different results based on the input type.

function processInput(input: string | number): number {
    if (typeof input === "string") {
        return input.length;
    } else {
        return input * input;
    }
}

Output:

// Results will appear here

Professional TypeScript Editor

TypeScript Editor
Output
// Output will appear here
TypeScript is compiled to JavaScript in real-time

TypeScript Assistant

Hi there! I'm your TypeScript assistant. Ask me about TypeScript features, syntax, or best practices. Try asking:
  • "What are union types?"
  • "Show me an example of generics"
  • "How do type guards work?"