|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/types
$cat docs/typescript-—-types-&-annotations.md
updated Recently·14 min read·published

TypeScript — Types & Annotations

TypeScriptBeginner
Introduction

TypeScript's type system is its core feature. Types describe the shape of your data — what values a variable can hold, what arguments a function accepts, and what it returns. TypeScript uses these types to catch errors at compile time, provide autocompletion, and document your code.

Unlike dynamically typed languages, TypeScript types are checked before code runs. The compiler (or your IDE) enforces type constraints, catching bugs like accessing undefined properties, passing wrong argument types, or misusing return values.

📝

note

TypeScript types are erased at runtime — they have zero performance cost. Your compiled JavaScript runs exactly as if you wrote it by hand.
Primitive Types

TypeScript has several primitive types corresponding to JavaScript's basic value types. Each has distinct behavior and use cases.

primitives.ts
TypeScript
1// string — text values
2let name: string = "Alice";
3let greeting: string = `Hello, ${name}`;
4
5// number — all numeric values (integers and floats, no distinction)
6let age: number = 30;
7let pi: number = 3.14159;
8let hex: number = 0xff;
9let billion: number = 1_000_000_000; // underscore separators
10
11// boolean — true/false
12let active: boolean = true;
13let deleted: boolean = false;
14
15// null and undefined
16let nothing: null = null;
17let notDefined: undefined = undefined;
18
19// bigint — arbitrary-precision integers (ES2020+)
20let big: bigint = 9007199254740991n;
21
22// symbol — unique identifiers
23let id: symbol = Symbol("id");
TypeExampleNotes
string"hello", `template`Unicode strings
number42, 3.14, NaN, InfinityAll numbers are 64-bit floats
booleantrue, falseLogical values
nullnullIntentional absence of value
undefinedundefinedUninitialized variable
voidreturn value of functions with no returnCannot be assigned a value
neverfunction that never returnsFor unreachable code
anyescape hatchOpts out of type checking — avoid
unknowntype-safe anyMust narrow before use
special_types.ts
TypeScript
1// void — no return value
2function log(message: string): void {
3 console.log(message);
4}
5
6// never — function never returns (throws, infinite loop, or type guard)
7function throwError(msg: string): never {
8 throw new Error(msg);
9}
10
11function infiniteLoop(): never {
12 while (true) {}
13}
14
15// any — opts out of type checking (avoid!)
16let anything: any = 42;
17anything = "now a string";
18anything.does.not.exist; // no error — but will crash at runtime
19
20// unknown — type-safe alternative to any
21let value: unknown = 42;
22// value.toUpperCase(); // Error: cannot use unknown directly
23if (typeof value === "string") {
24 value.toUpperCase(); // OK after narrowing

warning

Avoid any — it defeats the purpose of TypeScript. Use unknown when you genuinely don't know the type, then narrow with typeof or instanceof.
Arrays & Tuples

Arrays hold ordered collections of the same type. Tuples are fixed-length arrays where each position has a specific type.

arrays_tuples.ts
TypeScript
1// Arrays — two syntaxes (both equivalent)
2let numbers: number[] = [1, 2, 3];
3let names: Array<string> = ["Alice", "Bob"];
4
5// Read-only arrays
6let readonly_nums: readonly number[] = [1, 2, 3];
7// readonly_nums.push(4); // Error: Property 'push' does not exist
8
9// Tuples — fixed-length, typed arrays
10let person: [string, number] = ["Alice", 30];
11// person = [30, "Alice"]; // Error: wrong order
12
13// Named tuple elements (documentation only, not enforced at runtime)
14let point: [x: number, y: number, z: number] = [1, 2, 3];
15
16// Tuples with optional elements
17let record: [string, number?] = ["Alice"]; // OK
18let record2: [string, number?] = ["Alice", 30]; // OK
19
20// Rest elements in tuples
21let rest: [string, ...number[]] = ["Alice", 1, 2, 3];
22
23// Destructuring tuples
24let [firstName, age] = person;
25console.log(firstName); // Alice
26console.log(age); // 30

info

Use readonly number[] or ReadonlyArray<T>when you don't want to allow mutations. It's a compile-time check only — the array is still mutable at runtime.
Object Types

Object types describe the shape of objects — what properties they have and what types those properties are. TypeScript uses structural typing, so any object with the matching shape satisfies the type.

object_types.ts
TypeScript
1// Inline object type
2let user: { name: string; age: number; email?: string } = {
3 name: "Alice",
4 age: 30,
5};
6
7// Index signatures — dynamic keys
8let scores: { [subject: string]: number } = {
9 math: 95,
10 science: 88,
11};
12
13// Record utility type (cleaner alternative)
14let counts: Record<string, number> = {
15 views: 100,
16 likes: 25,
17};
18
19// Nested objects
20let config: {
21 db: { host: string; port: number };
22 debug: boolean;
23} = {
24 db: { host: "localhost", port: 5432 },
25 debug: true,
26};
27
28// Readonly properties
29let immutable: { readonly id: number; name: string } = {
30 id: 1,
31 name: "Alice",
32};
33// immutable.id = 2; // Error: Cannot assign to 'id'
Type Annotations vs Inference

TypeScript can usually figure out types automatically (inference). You only need explicit annotations when the compiler can't infer or infers something too broad. The general rule: annotate when it helps, skip when it's obvious.

annotations_inference.ts
TypeScript
1// Inference — TypeScript figures it out
2let x = 42; // inferred: number
3let s = "hello"; // inferred: string
4let arr = [1, 2, 3]; // inferred: number[]
5let obj = { a: 1, b: 2 }; // inferred: { a: number; b: number }
6
7// Inference from context
8const nums = [1, 2, 3].map(n => n * 2); // inferred: number[]
9
10// Annotations needed when:
11// 1. Empty initialization
12let result: string;
13result = getFromAPI(); // compiler needs to know the type
14
15// 2. Function return types (recommended for public APIs)
16function processData(input: string[]): { count: number; items: string[] } {
17 return { count: input.length, items: input };
18}
19
20// 3. Complex types that benefit from documentation
21type Config = {
22 host: string;
23 port: number;
24 ssl: boolean;
25};
26
27function createServer(config: Config): void {
28 // ...
29}
30
31// 4. When inference is too broad
32let data = JSON.parse("{}"); // inferred: any — annotate instead
33let typed: Record<string, unknown> = JSON.parse("{}");
34
35// Best practice: annotate function signatures, skip variable types when obvious
36function greet(name: string): string { // annotate params + return
37 return `Hello, ${name}`; // skip — inference is fine
38}

best practice

Annotate function parameters and return types — they serve as documentation and catch regressions. For local variables, let TypeScript infer unless the type is complex or the initial value is any.
Union & Intersection Types

Union types say "this can be one of several types." Intersection types say "this has all of these types combined." They are fundamental building blocks for composing types.

union_intersection.ts
TypeScript
1// Union — value can be one of multiple types
2let id: string | number;
3id = "abc"; // OK
4id = 123; // OK
5// id = true; // Error
6
7// Union with null/undefined (nullable)
8let name: string | null = null;
9name = "Alice"; // OK
10name = null; // OK
11
12// Function with union parameter
13function formatId(id: string | number): string {
14 if (typeof id === "string") {
15 return id.toUpperCase(); // TS knows: string
16 }
17 return id.toString(); // TS knows: number
18}
19
20// Intersection — combines all types into one
21type HasName = { name: string };
22type HasAge = { age: number };
23type Person = HasName & HasAge;
24
25let person: Person = {
26 name: "Alice", // from HasName
27 age: 30, // from HasAge
28};
29
30// Intersection of interfaces
31interface Logger {
32 log(message: string): void;
33}
34
35interface Formatter {
36 format(data: unknown): string;
37}
38
39type LogFormatter = Logger & Formatter;
40
41// Union vs Intersection decision:
42// Union: "A or B" — use | (e.g., string | number)
43// Intersection: "A and B" — use & (e.g., HasName & HasAge)

info

Unions are much more common than intersections. Most of the time you want "this value is one of these types," not "this value satisfies all of these types simultaneously."
Type Assertions

Type assertions tell TypeScript "trust me, I know the type." They don't change the runtime value — they only change how the compiler treats it. Use them sparingly; they bypass type safety.

assertions.ts
TypeScript
1// as syntax (preferred)
2let value: unknown = "hello";
3let length: number = (value as string).length;
4
5// angle-bracket syntax (avoid in TSX/JSX files)
6let length2: number = (<string>value).length;
7
8// Non-null assertion (!) — asserts value is not null/undefined
9let element = document.getElementById("app")!;
10element.innerHTML = "Hello"; // TS trusts it's not null
11
12// When assertions are useful:
13// 1. DOM queries (getElementById returns HTMLElement | null)
14let input = document.querySelector("input") as HTMLInputElement;
15input.value = "default";
16
17// 2. JSON parsing (returns any)
18let data = JSON.parse(response) as User;
19
20// 3. When you know more than the compiler
21let config = getEnvConfig() as { host: string; port: number };
22
23// BAD: overusing assertions
24let num: string | number = "hello";
25// (num as number).toFixed(2); // WRONG — will crash at runtime!
26
27// BETTER: use type narrowing
28if (typeof num === "number") {
29 num.toFixed(2); // safe
30}

warning

Type assertions are not type checks — they are type claims. If your claim is wrong, you get a runtime error. Prefer typeof / instanceof checks over assertions when possible.
Type Aliases vs Interfaces

Both type and interface create named types. For simple object shapes, they are largely interchangeable. The differences matter in specific scenarios.

type_vs_interface.ts
TypeScript
1// Type alias — flexible, can represent anything
2type ID = string | number;
3type Point = { x: number; y: number };
4type Callback = (data: string) => void;
5
6// Interface — object shapes only, supports declaration merging
7interface User {
8 name: string;
9 age: number;
10}
11
12// Declaration merging — interfaces combine automatically
13interface User {
14 email: string;
15}
16// User now has: name, age, email
17
18// Extending
19interface Admin extends User {
20 role: "admin";
21}
22
23// Interface with methods
24interface Repository {
25 findById(id: string): User | null;
26 findAll(): User[];
27 save(user: User): void;
28}
29
30// When to use which:
31// type: unions, intersections, mapped types, anything non-object
32// interface: object shapes, class contracts, when you need declaration merging
33
34// Declaration merging is unique to interfaces:
35interface Window {
36 myCustomProp: string;
37}
38// This extends the global Window type — only works with interfaces

best practice

Use interface for object shapes that might be extended or implemented by classes. Use type for unions, intersections, and complex type compositions. Pick one convention and stick with it in your codebase.
Best Practices

These guidelines help you write clean, type-safe TypeScript that catches real bugs without becoming a type annotation burden.

GuidelineWhy
Enable strict modeCatches the most errors from the start
Avoid anyDefeats type checking — use unknown instead
Annotate function signaturesDocuments public API, catches regressions
Let inference workSkip obvious annotations on local variables
Narrow before useUse typeof/instanceof instead of assertions
Prefer unknown over anyRequires narrowing — safer by default
Use readonly for immutabilityPrevents accidental mutations
Use branded types for IDsPrevents mixing up UserId and OrderId
best_practices.ts
TypeScript
1// Good: explicit function signatures
2function getUser(id: string): User | null {
3 return db.users.find(u => u.id === id) ?? null;
4}
5
6// Bad: any everywhere
7function getUser(id: any): any {
8 return db.users.find((u: any) => u.id === id);
9}
10
11// Good: unknown + narrowing
12function parse(input: unknown): string {
13 if (typeof input === "string") return input;
14 if (typeof input === "number") return input.toString();
15 throw new Error("Invalid input");
16}
17
18// Good: readonly for data that shouldn't change
19type Config = {
20 readonly apiUrl: string;
21 readonly timeout: number;
22};
23
24// Good: branded types for IDs
25type UserId = string & { __brand: "UserId" };
26type OrderId = string & { __brand: "OrderId" };
27
28function createUserId(id: string): UserId {
29 return id as UserId;
30}
$Blueprint — Engineering Documentation·Section ID: TS-TYPES·Revision: 1.0