TypeScriptType Annotations

Type Annotations

A type annotation is an explicit declaration you attach to a variable, parameter, or return position that tells TypeScript exactly what type a value should be. While TypeScript can infer many types automatically, annotations are the authoritative, self-documenting way to lock in a contract.

Syntax Overview

Type annotations use a colon (:) followed by the type. They can appear after variable names, function parameters, and return positions.

TS
// Variable annotations
let age: number = 30;
let username: string = "alice";
let isAdmin: boolean = false;

// Function parameter and return type annotations
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function
const greet = (name: string): string => `Hello, ${name}!`;
Note
When TypeScript can infer the type from the right-hand side, you do not need to annotate. Annotations shine at API boundaries and when you want to enforce a type that is wider or narrower than what inference would produce.
Primitive Type Annotations

TypeScript's three core primitive types map directly to JavaScript's primitives.

TypeScript type

JavaScript typeof

Examples

number

"number"

42, 3.14, -7, Infinity, NaN

string

"string"

'"hello"', '"", template

boolean

"boolean"

true, false

bigint

"bigint"

100n, BigInt(9007199254740991)

symbol

"symbol"

Symbol("id")

TS
let score: number = 99;
let title: string = "TypeScript Deep Dive";
let published: boolean = true;
let uniqueId: symbol = Symbol("id");
let bigNum: bigint = 9007199254740993n;
Array and Tuple Annotations

Arrays have two equivalent annotation syntaxes. Tuples let you annotate arrays with a fixed number of elements of specific types at each position.

TS
// Array — two equivalent forms
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];

// Readonly array
let frozen: readonly number[] = [10, 20, 30];
// frozen.push(40); // Error!

// Tuple — fixed length, positional types
let point: [number, number] = [10, 20];
let entry: [string, number] = ["Alice", 42];

// Named tuple elements (TypeScript 4.0+)
let range: [start: number, end: number] = [0, 100];

// Optional tuple elements
let optEntry: [string, number?] = ["Bob"];  // second element optional

// Rest elements in tuples
type StringsAndNumber = [string, ...string[], number];
let data: StringsAndNumber = ["a", "b", "c", 42];
Tip
Prefer named tuple elements when the positional meaning is not immediately obvious — they improve the hover tooltip in editors.
Object Type Annotations

Object types describe the shape of an object: which properties it has, their types, and whether they are optional or readonly.

TS
// Inline object type annotation
let user: { id: number; name: string; email?: string } = {
  id: 1,
  name: "Alice",
};

// Optional property (?)
function createUser(name: string, role?: string) {
  return { name, role: role ?? "viewer" };
}

// Readonly property
let config: { readonly host: string; port: number } = {
  host: "localhost",
  port: 3000,
};
// config.host = "prod"; // Error: Cannot assign to 'host' because it is read-only
Note
Inline object types work for one-off shapes. For reusable shapes, define an interface or type alias instead.
Function Type Annotations

You can annotate function parameters, return types, and entire function-type shapes.

TS
// Parameter and return type
function multiply(x: number, y: number): number {
  return x * y;
}

// void — function returns no meaningful value
function log(message: string): void {
  console.log(message);
}

// never — function never returns (throws or loops forever)
function fail(msg: string): never {
  throw new Error(msg);
}

// Annotating a function variable with a function type
let transform: (input: string) => string;
transform = (s) => s.toUpperCase(); // OK

// Optional parameter
function greet(name: string, greeting?: string): string {
  return `${greeting ?? "Hello"}, ${name}!`;
}

// Default parameter (type inferred from default value)
function repeat(text: string, times: number = 1): string {
  return text.repeat(times);
}

// Rest parameter
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
Union and Intersection Annotations

A union annotation (A | B) means the value can be either type. An intersection (A & B) means it must satisfy both types simultaneously.

TS
// Union
let id: number | string = 123;
id = "abc"; // also OK

function formatId(id: number | string): string {
  return typeof id === "number" ? id.toString() : id;
}

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

const alice: Person = { name: "Alice", age: 30 };
Type Aliases vs. Inline Annotations

You can annotate inline or factor the type out into a named alias. Both are equivalent, but aliases improve readability and reuse.

TS
// Inline — fine for one-off use
function process(input: { id: number; value: string }): void {
  console.log(input.id, input.value);
}

// Type alias — better when reused
type InputRecord = { id: number; value: string };
function process2(input: InputRecord): void {
  console.log(input.id, input.value);
}

// The two are identical at the type level
Annotating Class Members

TS
class Counter {
  count: number = 0;
  readonly name: string;

  constructor(name: string) {
    this.name = name;
  }

  increment(): void {
    this.count++;
  }

  getValue(): number {
    return this.count;
  }
}

const c = new Counter("hits");
c.increment();
console.log(c.getValue()); // 1
Annotating with Interfaces

Interfaces are the idiomatic way to annotate object shapes, especially for classes and public APIs.

TS
interface Point {
  x: number;
  y: number;
}

interface Shape {
  color: string;
  area(): number;
}

// Combining both with intersection
type ColoredPoint = Point & Shape;

// Using the interface as an annotation
function drawPoint(p: Point): void {
  console.log(`(${p.x}, ${p.y})`);
}

drawPoint({ x: 10, y: 20 }); // OK
Annotating async Functions and Promises

TS
// The return type of an async function is always Promise<T>
async function fetchUser(id: number): Promise<{ name: string; email: string }> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// Annotate Promise directly when not using async/await
function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// Async arrow function
const getData = async (url: string): Promise<unknown> => {
  const res = await fetch(url);
  return res.json();
};
Tip
Always annotate the return type of async functions. TypeScript can infer it, but making it explicit prevents surprises when the return type changes during refactoring.
Special Type Annotations

Annotation

Meaning

Use when

any

Opts out of type checking entirely

Migrating JS; avoid in new code

unknown

Like any but safe — must narrow before use

Parsing external data, catch blocks

never

Value that never occurs

Exhaustive checks, functions that always throw

void

Absence of a meaningful return value

Functions called for side effects

undefined

The undefined primitive

Optional fields with strictNullChecks

null

The null primitive

Fields that may be intentionally absent

object

Any non-primitive

Rarely useful — prefer a shaped type

TS
// unknown — safe alternative to any
function parseJSON(raw: string): unknown {
  return JSON.parse(raw);
}

const data = parseJSON('{"count": 42}');
// data.count // Error: Object is of type 'unknown'
if (typeof data === "object" && data !== null && "count" in data) {
  console.log((data as { count: number }).count); // 42
}

// never — exhaustive check helper
type Color = "red" | "green" | "blue";
function handleColor(c: Color): string {
  switch (c) {
    case "red":   return "#f00";
    case "green": return "#0f0";
    case "blue":  return "#00f";
    default: {
      const _exhaustive: never = c;
      throw new Error(`Unhandled color: ${_exhaustive}`);
    }
  }
}
Annotation Best Practices
  1. Annotate function parameters always — they are never inferred from call sites

  2. Annotate function return types on public/exported functions

  3. Let TypeScript infer local variable types when the initializer makes it obvious

  4. Prefer interface for object shapes that may be extended; prefer type for unions and intersections

  5. Avoid any — use unknown and narrow, or use a specific type

  6. Use readonly wherever mutation is not intended

  7. Add ? only when a value is genuinely optional, not as a workaround for missing data

Warning
Overusing any defeats the purpose of TypeScript. Audit your codebase regularly with noImplicitAny: true in tsconfig to surface accidental any.
Annotating Generics

When calling generic functions or instantiating generic classes, you can provide explicit type arguments in angle brackets. Most of the time TypeScript infers them, but explicit annotations clarify intent or force a wider type.

TS
// TypeScript infers T from the argument
const nums = [1, 2, 3];               // number[]
const set  = new Set([1, 2, 3]);      // Set<number>

// Explicit type argument — useful when inference is ambiguous
const set2   = new Set<string>();     // Set<string> — no items to infer from
const map    = new Map<string, number>(); // Map<string, number>

// Generic function with explicit argument
function createList<T>(item: T, size: number): T[] {
  return Array.from({ length: size }, () => item);
}

const strings = createList<string>("x", 5); // string[]
const numbers = createList<number>(0, 10);  // number[]

// Generic interface annotation
interface Repository<T> {
  find(id: number): Promise<T | null>;
  save(entity: T): Promise<T>;
  delete(id: number): Promise<void>;
}

class UserRepository implements Repository<User> {
  async find(id: number): Promise<User | null> { /* ... */ return null; }
  async save(user: User): Promise<User> { return user; }
  async delete(id: number): Promise<void> { /* ... */ }
}
Annotating with Mapped and Conditional Types

TS
// Annotating a variable with a mapped type
type Flags<T> = { [K in keyof T]: boolean };
const userFlags: Flags<User> = { id: true, name: false, email: true };

// Conditional type annotation
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

// Using ReturnType — built-in annotation utility
function computeTotal(price: number, tax: number): { total: number; vat: number } {
  return { total: price + tax, vat: tax };
}
type TotalResult = ReturnType<typeof computeTotal>;
// TotalResult = { total: number; vat: number }

// Annotating with Parameters utility
type ComputeParams = Parameters<typeof computeTotal>;
// ComputeParams = [price: number, tax: number]
Summary
  • Type annotations use a colon after the name and before the value

  • Parameters always need annotations; local variables often do not

  • Arrays use T[] or Array<T>; tuples use [T1, T2, ...]

  • Object shapes can be inline or extracted into interfaces and type aliases

  • void marks side-effect functions; never marks unreachable code

  • unknown is the safe alternative to any — always narrow before use

  • Annotate return types of async functions to document the resolved value type