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.
// 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}!`;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"', '"", |
boolean | "boolean" | true, false |
bigint | "bigint" | 100n, BigInt(9007199254740991) |
symbol | "symbol" | Symbol("id") |
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.
// 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];
Object Type Annotations
Object types describe the shape of an object: which properties it has, their types, and whether they are optional or readonly.
// 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-onlyinterface or type alias instead.Function Type Annotations
You can annotate function parameters, return types, and entire function-type shapes.
// 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.
// 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.
// 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 levelAnnotating Class Members
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()); // 1Annotating with Interfaces
Interfaces are the idiomatic way to annotate object shapes, especially for classes and public APIs.
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 }); // OKAnnotating async Functions and Promises
// 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();
};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 |
// 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
Annotate function parameters always — they are never inferred from call sites
Annotate function return types on public/exported functions
Let TypeScript infer local variable types when the initializer makes it obvious
Prefer interface for object shapes that may be extended; prefer type for unions and intersections
Avoid any — use unknown and narrow, or use a specific type
Use readonly wherever mutation is not intended
Add ? only when a value is genuinely optional, not as a workaround for missing data
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.
// 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
// 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