TypeScript Cheat Sheet
TypeScript 5.x reference covering basic types, unions, interfaces, generics, narrowing, utility types, and tsconfig essentials.
TypeScript is JavaScript with a structural type system that compiles away. Lean on unknown over any, prefer narrowing to type assertions, and let tsc --strict catch the mistakes that runtime tests usually miss.
Basic types
The primitive types mirror JavaScript: string, number, boolean, plus the structural types array, tuple, object, and the bottom and top helpers never / unknown / any / void.
// Primitives
const name: string = "Ada";
const age: number = 36;
const active: boolean = true;
const big: bigint = 100n;
const sym: symbol = Symbol("id");
// Arrays and tuples
const tags: string[] = ["ts", "js"];
const pair: [string, number] = ["x", 1];
// Special types
let v: any = "anything"; // opt out of type checking — avoid
let u: unknown = fetchJson(); // top type, must narrow before use
function fail(): never { throw new Error("nope"); } // bottom type
function log(): void { console.log("hi"); } // no return value
| Type | Use for |
|---|---|
string | Text data |
number | All numeric values (no int/float split) |
bigint | Integers larger than 2^53 − 1 |
boolean | true / false |
string[] or Array<string> | List of strings |
[A, B] | Fixed-length record with known position types |
unknown | Untrusted input — narrow before use |
any | Escape hatch — turns off the checker |
never | Function that never returns normally |
void | Function returns nothing meaningful |
Union and intersection types
A union A | B is "either A or B"; an intersection A & B is "both A and B at once". Reach for unions first — they model real alternatives cleanly with discriminated narrowing.
// Union: one of several shapes
type Id = string | number;
type Result = { ok: true; value: string } | { ok: false; error: Error };
// Intersection: combine capability types
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type User = { id: string; name: string } & WithTimestamps;
Use a discriminant field when union members have a common shape — narrowing becomes exhaustive and safe.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "square": return s.side ** 2;
case "triangle": return (s.base * s.height) / 2;
}
}
Type aliases vs interfaces
Both define object types; the choice mostly comes down to intent. Use interface for public API shapes that callers may extend, and type for unions, intersections, and computed shapes.
interface User {
readonly id: string;
name: string;
email?: string; // optional
}
type Point = { x: number; y: number };
// Extension with interface
interface AdminUser extends User {
permissions: string[];
}
// Equivalent with intersection type
type AdminUser2 = User & { permissions: string[] };
// Implementation via declaration merging
interface Window { appVersion: string }
| Use | type | interface |
|---|---|---|
| Object shape | ✓ | ✓ |
| Union / intersection | ✓ | ✗ |
| Computed via mapped/conditional types | ✓ | ✗ |
| Declaration merging | ✗ | ✓ |
implements on classes | ✓ | ✓ |
Extends via extends | via intersection | via extends |
Functions and optional parameters
Annotate parameters and return types at the boundaries (public APIs, exported functions) and let inference work inside. Optional parameters go at the end of the parameter list.
function add(a: number, b: number): number {
return a + b;
}
const greet = (name: string = "World"): string => `Hello, ${name}`;
// Optional with `?`, default with `=`
function load(key: string, opts?: { cache?: boolean }): string {
return `${key}:${opts?.cache ?? true}`;
}
// Rest parameters must be typed as an array
function tag(prefix: string, ...parts: string[]): string {
return [prefix, ...parts].join(":");
}
| Form | Behavior |
|---|---|
(a: A, b: B) => R | Inline function type |
function f(a: A): R { ... } | Declaration with body |
param?: T | Optional, may be undefined |
param: T = default | Default value if omitted |
...rest: T[] | Rest must be an array type |
Generics
Generics parameterize types so the same code works on multiple shapes. Use extends to constrain what the type parameter can be.
function identity<T>(value: T): T {
return value;
}
// Constrained generic
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
// Generic interface
interface Repository<T> {
get(id: string): Promise<T | null>;
save(item: T): Promise<void>;
}
// Multiple type parameters
function zip<A, B>(a: A[], b: B[]): Array<[A, B]> {
return a.map((x, i) => [x, b[i]]);
}
| Construct | Example |
|---|---|
| Generic function | function f<T>(x: T): T |
| Generic interface | interface Box<T> { value: T } |
| Generic class | class Stack<T> { ... } |
| Default | function f<T = string>() {} |
| Constraint | <T extends { id: string }> |
Narrowing
Narrowing is how TypeScript refines a broad type into a specific one based on runtime checks. It is the alternative to type assertions and almost always the better one.
function print(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // value: string
} else {
console.log(value.toFixed(2)); // value: number
}
}
class Dog { bark() {} }
class Cat { meow() {} }
function speak(animal: Dog | Cat) {
if (animal instanceof Dog) animal.bark();
else animal.meow();
}
interface ApiOk { ok: true; data: unknown }
interface ApiFail { ok: false; error: Error }
function handle(r: ApiOk | ApiFail) {
if (r.ok) {
// r.data is reachable here
} else {
console.error(r.error);
}
}
| Predicate | Narrows by |
|---|---|
typeof x === "string" | Primitive type |
x instanceof C | Class identity |
"key" in x | Property presence |
Array.isArray(x) | Array vs non-array |
Discriminant equality (r.kind === "x") | Tagged union members |
Avoid type assertions (value as T) where narrowing works. Assertions are unchecked: if the runtime shape doesn't match T, the error happens at the worst possible moment.
Utility types
The standard library ships a small toolbox of generic types that build new types from existing ones. Reach for them before writing your own mapped type.
interface User {
id: number;
name: string;
email?: string;
}
type UserUpdate = Partial<User>; // all optional
type CreateUser = Omit<User, "id">; // drop a key
type UserPreview = Pick<User, "id" | "name">; // keep only some
type RequiredUser = Required<User>; // all required
type ReadonlyUser = Readonly<User>; // no mutation
type UsersById = Record<string, User>; // dictionary shape
type UserValues = ReturnType<typeof getUser>; // return type of fn
type Awaited<T> = T extends Promise<infer U> ? U : T; // unwrap promise
| Utility | Effect |
|---|---|
Partial<T> | All properties optional |
Required<T> | All properties required |
Readonly<T> | All properties read-only |
Pick<T, K> | Subset of keys |
Omit<T, K> | All keys except K |
Record<K, V> | Map of K to V |
Exclude<U, X> | Remove types from a union |
Extract<U, X> | Keep only types assignable to X |
ReturnType<F> | Return type of function F |
Parameters<F> | Tuple of F's parameter types |
Awaited<T> | Unwrap Promise<T> recursively |
tsconfig.json essentials
tsconfig.json controls how tsc compiles. strict: true enables the family of strictness flags that catch real bugs; everything else is project taste.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
| Option | Effect |
|---|---|
target | ECMAScript version emitted |
module | Module system in output (ESNext, CommonJS, NodeNext) |
moduleResolution | Algorithm (Bundler, Node, NodeNext) |
lib | Type libraries pulled in (DOM, ES2022, …) |
strict | Enables strictNullChecks, noImplicitAny, etc. |
noUncheckedIndexedAccess | arr[i] is T | undefined |
outDir / rootDir | Output layout |
skipLibCheck | Skip type-checking declaration files |
esModuleInterop | Sane default imports from CJS modules |
Type assertions exist but narrowing is better. Use as const to widen literals into their precise tuple/primitive types, and satisfies T (TS 4.9+) to check a value conforms to a type without losing its narrow inference.