Skip to main content

Type Annotations and Type Inference in TypeScript

Created: May 8, 2026 Larry Qu 8 min read

Type annotations and inference are core TypeScript features. This article covers type system fundamentals and advanced typing patterns.

Introduction

Type annotations and inference provide:

  • Type safety
  • Code documentation
  • IDE support
  • Error prevention
  • Better refactoring

Understanding types helps you:

  • Write type-safe code
  • Leverage IDE features
  • Prevent runtime errors
  • Document code
  • Improve maintainability

Primitive Types

Basic Type Annotations

// ✅ Good: Basic type annotations
const name: string = 'John';
const age: number = 30;
const isActive: boolean = true;
const nothing: null = null;
const undefined_value: undefined = undefined;

// ✅ Good: Literal types
const status: 'active' | 'inactive' = 'active';
const count: 1 | 2 | 3 = 2;

// ✅ Good: Any type (use sparingly)
let value: any = 'string';
value = 42; // OK but not recommended

// ✅ Good: Unknown type (safer than any)
let unknown_value: unknown = 'string';
// unknown_value.toUpperCase(); // Error: must check type first
if (typeof unknown_value === 'string') {
  unknown_value.toUpperCase(); // OK
}

Type Inference

// ✅ Good: Type inference
const message = 'Hello'; // inferred as string
const count = 42; // inferred as number
const isActive = true; // inferred as boolean

// ✅ Good: Inferred from function return
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

const result = add(5, 3); // result inferred as number

// ✅ Good: Inferred from array
const numbers = [1, 2, 3]; // inferred as number[]
const mixed = [1, 'two', true]; // inferred as (number | string | boolean)[]

Complex Types

Arrays and Tuples

// ✅ Good: Array types
const strings: string[] = ['a', 'b', 'c'];
const numbers: Array<number> = [1, 2, 3];
const mixed: (string | number)[] = ['a', 1, 'b'];

// ✅ Good: Tuple types
const tuple: [string, number] = ['hello', 42];
const tuple2: [string, number, boolean] = ['hello', 42, true];

// ✅ Good: Tuple with optional elements
const optional: [string, number?] = ['hello'];
const optional2: [string, number?] = ['hello', 42];

// ✅ Good: Tuple with rest elements
const rest: [string, ...number[]] = ['hello', 1, 2, 3];

Objects

// ✅ Good: Object type annotations
const user: { name: string; age: number } = {
  name: 'John',
  age: 30
};

// ✅ Good: Optional properties
const config: { host: string; port?: number } = {
  host: 'localhost'
};

// ✅ Good: Readonly properties
const point: { readonly x: number; readonly y: number } = {
  x: 10,
  y: 20
};
// point.x = 30; // Error: cannot assign to readonly property

// ✅ Good: Index signatures
const map: { [key: string]: number } = {
  a: 1,
  b: 2,
  c: 3
};

Union and Intersection Types

// ✅ Good: Union types
type Status = 'active' | 'inactive' | 'pending';
const status: Status = 'active';

type ID = string | number;
const userId: ID = 123;
const postId: ID = 'abc-123';

// ✅ Good: Intersection types
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;

const person: Person = {
  name: 'John',
  age: 30
};

// ✅ Good: Union with type guards
function process(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase();
  } else {
    return value * 2;
  }
}

Functions

Function Type Annotations

// ✅ Good: Function parameter and return types
function add(a: number, b: number): number {
  return a + b;
}

// ✅ Good: Optional parameters
function greet(name: string, greeting?: string): string {
  return `${greeting || 'Hello'}, ${name}!`;
}

// ✅ Good: Default parameters
function multiply(a: number, b: number = 1): number {
  return a * b;
}

// ✅ Good: Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((a, b) => a + b, 0);
}

// ✅ Good: Function type
type Callback = (value: string) => void;
const callback: Callback = (value) => {
  console.log(value);
};

// ✅ Good: Function overloading
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  return value * 2;
}

Type Guards

Type Narrowing

// ✅ Good: typeof guard
function process(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase(); // value is string
  } else {
    return value * 2; // value is number
  }
}

// ✅ Good: instanceof guard
class Dog {
  bark() { }
}

class Cat {
  meow() { }
}

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

// ✅ Good: Custom type guard
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function process(value: unknown) {
  if (isString(value)) {
    return value.toUpperCase(); // value is string
  }
}

// ✅ Good: Discriminated unions
type Circle = { kind: 'circle'; radius: number };
type Square = { kind: 'square'; side: number };
type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.side ** 2;
  }
}

Advanced Types

Conditional Types

// ✅ Good: Conditional types
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<42>; // false

// ✅ Good: Conditional with generics
type Flatten<T> = T extends Array<infer U> ? U : T;

type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number

Mapped Types

// ✅ Good: Mapped types
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type User = { name: string; age: number };
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number }

// ✅ Good: Getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

Utility Types

// ✅ Good: Built-in utility types
type User = { name: string; age: number; email: string };

// Partial - all properties optional
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }

// Required - all properties required
type RequiredUser = Required<PartialUser>;
// { name: string; age: number; email: string }

// Readonly - all properties readonly
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

// Pick - select specific properties
type UserPreview = Pick<User, 'name' | 'email'>;
// { name: string; email: string }

// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { name: string; age: number }

// Record - create object with specific keys
type Status = 'active' | 'inactive' | 'pending';
type StatusCount = Record<Status, number>;
// { active: number; inactive: number; pending: number }

// Exclude - exclude types from union
type NonString = Exclude<string | number | boolean, string>;
// number | boolean

// Extract - extract types from union
type StringOrNumber = Extract<string | number | boolean, string | number>;
// string | number

// ReturnType - get function return type
type AddReturn = ReturnType<typeof add>;
// number

Practical Examples

Type-Safe API Response

// ✅ Good: Type-safe API response
interface ApiResponse<T> {
  status: 'success' | 'error';
  data?: T;
  error?: string;
}

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

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();

    return {
      status: 'success',
      data
    };
  } catch (error) {
    return {
      status: 'error',
      error: error instanceof Error ? error.message : 'Unknown error'
    };
  }
}

// Usage
const response = await fetchUser(1);
if (response.status === 'success' && response.data) {
  console.log(response.data.name); // Type-safe access
}

Type-Safe Event Handler

// ✅ Good: Type-safe event handler
type EventMap = {
  'user:login': { userId: number };
  'user:logout': { timestamp: number };
  'error': { message: string };
};

class EventEmitter {
  private listeners: Map<string, Function[]> = new Map();

  on<K extends keyof EventMap>(
    event: K,
    listener: (data: EventMap[K]) => void
  ): void {
    if (!this.listeners.has(event as string)) {
      this.listeners.set(event as string, []);
    }
    this.listeners.get(event as string)!.push(listener);
  }

  emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
    const listeners = this.listeners.get(event as string) || [];
    listeners.forEach(listener => listener(data));
  }
}

// Usage
const emitter = new EventEmitter();

emitter.on('user:login', (data) => {
  console.log(`User ${data.userId} logged in`);
});

emitter.emit('user:login', { userId: 123 });

Best Practices

  1. Use explicit types:
    // ✅ Good
    const user: { name: string; age: number } = { name: 'John', age: 30 };
    
    // ❌ Bad
    const user: any = { name: 'John', age: 30 };
    ```javascript
    
  2. Leverage type inference:
    // ✅ Good
    const numbers = [1, 2, 3]; // inferred as number[]
    
    // ❌ Bad
    const numbers: any[] = [1, 2, 3];
    ```javascript
    
  3. Use union types:
    // ✅ Good
    type Status = 'active' | 'inactive';
    
    // ❌ Bad
    type Status = string;
    ```javascript
    

Common Mistakes

  1. Using any:
    // ❌ Bad
    const value: any = 'string';
    
    // ✅ Good
    const value: string = 'string';
    ```javascript
    
  2. Not using type guards:
    // ❌ Bad
    function process(value: string | number) {
      return value.toUpperCase(); // Error
    }
    
    // ✅ Good
    function process(value: string | number) {
      if (typeof value === 'string') {
        return value.toUpperCase();
      }
    }
    

Summary

Type annotations and inference are fundamental. Key takeaways:

  • Use explicit type annotations
  • Leverage type inference
  • Use union and intersection types
  • Implement type guards
  • Use utility types
  • Avoid any type
  • Use discriminated unions
  • Document with types

Next Steps

Resources

Comments

Share this article

Scan to read on mobile