▸ Primitive types docs ↗
let name: string = "alice" let age: number = 30 let ok: boolean = true let big: bigint = 9007199254740993n let sym: symbol = Symbol("id") let nothing: null = null let notSet: undefined = undefined
Special
any // turns off checking — avoid unknown // safe any — must narrow before use never // no value can exist (exhaustiveness) void // fn returns nothing usable object // any non-primitive
Avoid: the boxed types
String, Number, Boolean (capitalised). Always use the lowercase primitives.▸ Variables & const docs ↗
// Inferred — preferred const count = 42 // const → literal type 42 let total = 0 // let → widened to number // Explicit annotation const users: string[] = []
as const
const role = "admin" as const // type: "admin" const point = { x: 1, y: 2 } as const // { readonly x: 1; readonly y: 2 }
satisfies — type-check without widening
const palette = { red: [255, 0, 0], blue: "#00f", } satisfies Record<string, string | number[]> palette.red[0].toFixed() // ✓ still number[] palette.blue.toUpperCase() // ✓ still string — types preserved
▸ Object types docs ↗
type User = { id: number name: string email?: string // optional readonly createdAt: Date // readonly [key: string]: unknown // index signature }
Optional vs | undefined
// With exactOptionalPropertyTypes: true — these differ: type A = { name?: string } // key may be absent type B = { name: string | undefined } // key required, value may be undefined
Index signatures
type Headers = { [name: string]: string } type Matrix = { [row: number]: { [col: number]: number } }
▸ Arrays & tuples docs ↗
const a: number[] = [1, 2, 3] const b: Array<number> = [1, 2] const r: readonly number[] = [1, 2] // Tuple const pair: [string, number] = ["x", 1] // Named tuple (labels improve hover info) type Range = [start: number, end: number] // Variadic tuple type Args<T extends unknown[]> = [action: string, ...args: T]
▸ Functions docs ↗
// Declaration function add(a: number, b: number): number { return a + b } // Expression const sub = (a: number, b: number): number => a - b // Function type alias type BinaryOp = (a: number, b: number) => number // Optional / default / rest function greet(name: string, tag?: string, punct: string = "!"): string { ... } function sum(...xs: number[]): number { ... }
Overloads
function parse(s: string): number function parse(s: string[]): number[] function parse(s: string | string[]): number | number[] { ... }
Call signatures & constructors
type Logger = { (msg: string): void // callable level: "info" | "warn" } type UserCtor = new (id: number) => User // constructable
▸ type vs interface docs ↗
| type | interface | |
|---|---|---|
| Object shape | ✓ | ✓ |
| Union / primitive | ✓ | ✗ |
| Mapped / conditional | ✓ | ✗ |
| Declaration merging | ✗ | ✓ |
| extends | via & | native |
| Performance (large) | fine | slightly better |
interface Animal { name: string } interface Dog extends Animal { bark(): void } type Animal2 = { name: string } type Dog2 = Animal2 & { bark(): void } // Declaration merging — interface only: interface Window { myApi: Api }
Rule of thumb: use
interface for public, extensible object shapes (especially library types); use type for unions, intersections, and computed types.▸ Unions & intersections docs ↗
type Id = string | number // union type Named = { name: string } type Aged = { age: number } type Person = Named & Aged // intersection
Distributive unions
type Boxed<T> = { value: T } type BoxedSN = Boxed<string | number> // → { value: string } | { value: number } ❌ NO — generic doesn't distribute // Distribution only happens for naked type-params in conditional types: type Box<T> = T extends unknown ? { value: T } : never type Boxes = Box<string | number> // → { value: string } | { value: number }
▸ Literal types & narrowing docs ↗
type Direction = "north" | "south" | "east" | "west" type Bit = 0 | 1 type Status = true | false
Narrowing flow
function area(s: string | number): number { if (typeof s === "string") return s.length // s: string return s * s // s: number }
Narrowing operators
typeof— primitive checksinstanceof— class checks (respectsSymbol.hasInstance)in— property checkArray.isArray()- equality against literal:
x === "a" - truthy/falsy:
if (x)— narrows out null/undefined/0/""
Assertion functions
function assert(cond: unknown, msg?: string): asserts cond { if (!cond) throw new Error(msg) } assert(typeof input === "string") input.toUpperCase() // ✓ narrowed
▸ Type guards & predicates docs ↗
User-defined predicate
function isString(x: unknown): x is string { return typeof x === "string" }
Inferred predicates — 5.5+
const mixed = [1, "a", 2, "b"] const nums = mixed.filter(x => typeof x === "number") // nums: number[] ✓ (was (string|number)[] before 5.5)
Property-based narrowing
function hasName(x: unknown): x is { name: string } { return typeof x === "object" && x !== null && "name" in x && typeof (x as any).name === "string" }
▸ Discriminated unions docs ↗
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number } | { kind: "rect"; w: number; h: number } function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2 case "square": return s.side * s.side case "rect": return s.w * s.h default: return assertNever(s) } } function assertNever(x: never): never { throw new Error(`unhandled: ${x}`) }
Why this matters: if you add a new variant to
Shape and forget a case, the assertNever call fails at compile time — the un-handled variant won't be never.▸ Generics docs ↗
function identity<T>(x: T): T { return x } // Constraint function pluck<T, K extends keyof T>(o: T, k: K): T[K] { return o[k] } // Default type Result<T, E = Error> = { ok: true; value: T } | { ok: false; err: E }
const type parameters — 5.0+
function first<const T extends readonly unknown[]>(xs: T): T[0] { return xs[0] } const a = first(["a", "b", "c"]) // type: "a" (not string)
NoInfer<T> — 5.4+
function createState<T>(initial: T, values: NoInfer<T>[]) { ... } createState("red", ["red", "green", "blue"]) // T inferred from `initial`, not widened by `values`
▸ Built-in utility types docs ↗
| Type | Effect |
|---|---|
Partial<T> | all props optional |
Required<T> | all props required |
Readonly<T> | all props readonly |
Pick<T, K> | keep keys K |
Omit<T, K> | drop keys K |
Record<K, V> | object with key set K |
Exclude<T, U> | remove union members |
Extract<T, U> | keep matching members |
NonNullable<T> | strip null | undefined |
ReturnType<F> | fn return type |
Parameters<F> | fn args tuple |
ConstructorParameters<C> | ctor args |
InstanceType<C> | instance shape |
Awaited<P> | unwrap Promise (recursive) |
NoInfer<T> | block inference site (5.4+) |
Uppercase<S> | intrinsic string |
Lowercase<S> | " |
Capitalize<S> | " |
Uncapitalize<S> | " |
type UserPatch = Partial<User> type UserPublic = Omit<User, "password" | "token"> type ApiResult = Awaited<ReturnType< typeof fetchUser >>
▸ Conditional types & infer docs ↗
type IsString<T> = T extends string ? true : false // infer — extract part of a type type Return<F> = F extends (...a: any[]) => infer R ? R : never type First<T> = T extends [infer H, ...any[]] ? H : never type UnwrapP<T> = T extends Promise<infer U> ? U : T
Distribution control
// Naked T → distributes over union type ToArray<T> = T extends any ? T[] : never type A = ToArray<string | number> // string[] | number[] // Wrap in tuple → block distribution type ToArr2<T> = [T] extends [any] ? T[] : never type B = ToArr2<string | number> // (string | number)[]
▸ Mapped types docs ↗
// Basic type Optional<T> = { [K in keyof T]?: T[K] } // Modifiers: +readonly / -readonly / +? / -? type Mutable<T> = { -readonly [K in keyof T]: T[K] } type RequiredKeys<T> = { [K in keyof T]-?: T[K] } // Key remapping with `as` type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } // Getters<{name: string; age: number}> → // { getName(): string; getAge(): number } // Filter keys (omit functions) type DataOnly<T> = { [K in keyof T as T[K] extends Function ? never : K]: T[K] }
▸ Template literal types docs ↗
type Greeting = `hello, ${string}` type Route = `/${"users" | "posts"}/${string}` // CSS-like enums type Side = "top" | "right" | "bottom" | "left" type Margin = `margin-${Side}` // "margin-top" | "margin-right" | "margin-bottom" | "margin-left" // Parse with infer type Split<S, D extends string> = S extends `${infer H}${D}${infer R}` ? [H, ...Split<R, D>] : [S] type P = Split<"a.b.c", "."> // ["a","b","c"]
▸ Classes docs ↗
class Account { readonly id: string private #balance: number // JS private (truly hidden) protected log: string[] = [] constructor(public name: string, id: string) { this.id = id this.#balance = 0 } get balance(): number { return this.#balance } set balance(v: number) { this.#balance = v } static open(name: string): Account { return new Account(name, crypto.randomUUID()) } }
Abstract classes
abstract class Animal { abstract speak(): string describe(): string { return `I say ${this.speak()}` } }
implements vs extends
interface Logger { log(msg: string): void } class ConsoleLogger implements Logger { log(msg: string): void { console.log(msg) } }
Tip: prefer
#field (ECMAScript private) over private field — the former is enforced at runtime, the latter only at compile time.▸ Enums docs ↗
// Numeric enum enum Direction { Up, Down, Left, Right } // String enum (preferred — stable serialised values) enum Role { Admin = "admin", User = "user", Guest = "guest", } // const enum (inlined, no runtime object) — incompatible with isolatedModules const enum Flag { A = 1, B = 2 }
Modern alternative: for new code, prefer a string-literal union + const object — works under
erasableSyntaxOnly (5.8+) and avoids the runtime cost.
const Role = { Admin: "admin", User: "user" } as const type Role = typeof Role[keyof typeof Role]
▸ Modules docs ↗
// Named export const add = (a, b) => a + b import { add } from "./math.ts" // Default export default class User {} import User from "./user.ts" // Type-only — erased at runtime import type { User } from "./user.ts" export type { User } // Re-export export * from "./shapes.ts" export { add as sum } from "./math.ts"
Import attributes — 5.3+
import data from "./data.json" with { type: "json" } const mod = await import("./mod.ts", { with: { type: "json" } })
ESM relative imports under nodenext
// Always include the extension — Node strips types, doesn't resolve import { add } from "./math.ts" // or .js (transpiled)
▸ Async & Promises docs ↗
async function fetchUser(id: string): Promise<User> { const res = await fetch(`/api/users/${id}`) if (!res.ok) throw new Error(res.statusText) return res.json() as Promise<User> } // Awaited<T> — recursively unwraps type R = Awaited<Promise<Promise<number>>> // number // Top-level await — module-only const config = await loadConfig()
Promise.allSettled / .race / .any
const results = await Promise.allSettled([fetchUser("a"), fetchUser("b")]) for (const r of results) { if (r.status === "fulfilled") ...r.value else ...r.reason }
▸ Decorators stage 3 · 5.0+ docs ↗
function logged<This, Args extends unknown[], R>( target: (this: This, ...args: Args) => R, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R> ) { return function (this: This, ...args: Args): R { console.log(`call ${String(context.name)}`, args) return target.call(this, ...args) } } class Api { @logged fetch(id: string) { ... } }
Decorator context types
ClassDecoratorContextClassMethodDecoratorContextClassFieldDecoratorContextClassGetterDecoratorContext/…Setter…ClassAccessorDecoratorContext
Note: standard decorators (5.0+) are different from the legacy
experimentalDecorators form. They aren't compatible — pick one. Standard is the future.▸ using & resource management 5.2+ docs ↗
class FileHandle { constructor(public path: string) {} [Symbol.dispose]() { console.log(`closing ${this.path}`) } } function readConfig() { using handle = new FileHandle("/etc/app.conf") // ... do work ... // handle[Symbol.dispose]() runs here automatically } // Async variant await using conn = await openConnection() // conn[Symbol.asyncDispose]() awaited at scope exit
Helper types
Disposable // has [Symbol.dispose]() AsyncDisposable // has [Symbol.asyncDispose](): Promise<void>
▸ tsconfig essentials docs ↗
{ "compilerOptions": { "target": "ES2024", "module": "nodenext", "moduleResolution": "nodenext", "lib": ["ES2024"], "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true, "noFallthroughCasesInSwitch": true, "noPropertyAccessFromIndexSignature": true, "verbatimModuleSyntax": true, "isolatedModules": true, "erasableSyntaxOnly": true, // 5.8+ : forbid enums, namespaces, etc. "allowImportingTsExtensions": true, "rewriteRelativeImportExtensions": true, // 5.7+ : .ts → .js on emit "skipLibCheck": true, "noEmit": true } }
Strict family (turned on by "strict": true)
| Flag | What it does |
|---|---|
noImplicitAny | error on inferred any |
strictNullChecks | null/undefined not assignable to others |
strictFunctionTypes | contravariant param check |
strictBindCallApply | type-check .bind/.call/.apply |
strictPropertyInitialization | class fields must be assigned |
alwaysStrict | emit "use strict" |
useUnknownInCatchVariables | catch (e: unknown) |
▸ What's new — 5.x → 6.0 docs ↗
6.0 (current)
- Deprecation warnings for
outFile,module: UMD/AMD/System,moduleResolution: classic,target: ES5. All scheduled for removal in 7.0. - Escape hatch:
"ignoreDeprecations": "6.0"in tsconfig. - Many internal perf wins, faster project loading.
5.8
--erasableSyntaxOnly— bans constructs that can't be type-stripped (enums, namespaces, parameter properties).- Granular conditional return checks; better narrowing on nested returns.
5.7
--rewriteRelativeImportExtensions— emit-time.ts → .jsrewriting (lets you import with the source extension).- Never-initialized variable diagnostics.
- Path-based
tsconfig.jsoninheritance viaextends.
5.6
- Disallowed implicit nullish/truthy comparisons (
if (regex)→ warns). - Iterator helper methods typed (
.map,.filteron iterators). - Strict checks on built-in iterator return types.
5.5
- Inferred type predicates —
.filter(x => x != null)now narrows. - Regex syntax validation at compile time.
--isolatedDeclarations— fast.d.tsemit without full type-check.
5.4
NoInfer<T>intrinsic.- Preserved narrowing inside closures after last assignment.
5.3
- Import attributes:
import x from "./y" with { type: "json" }. - Narrowing using
Symbol.hasInstance.
5.2
using&await usingdeclarations;Disposable,AsyncDisposable.- Decorator metadata (
context.metadata). - Copying array methods:
.toSorted(),.toReversed(),.toSpliced(),.with().
5.1
- Easier return inference (functions returning
undefined). - Namespaced JSX attributes.
5.0
- Stage-3 decorators.
consttype parameters.- All enums become union enums.
--moduleResolution bundler.verbatimModuleSyntax.