TypeScript: Types, Generics, Utility Types and Narrowing Reference Guide
By DevShelfHub
Types, generics, utility types, narrowing, modules, tsconfig — the syntax you actually look up under deadline.
87 items
◷ 7 min
Types
Generics
Narrowing
Start hereQuick start · 6 you’ll reach for daily
Type a valueconst x: number = 1
Object shapetype T = { a: string }
Uniontype R = "ok" | "err"
Genericfunction id<T>(x: T): T
Keep literals{…} satisfies T
Runnpx tsx src/index.ts
Target versions · paceVersions
Targets:typescript ≥ 5.5node ≥ 20target: ES2022
Sheet assumes TypeScript 5.5+ — satisfies,
const type parameters,
noUncheckedIndexedAccess, NodeNext modules. For library
authors, tsconfig’s
declaration + composite
matter most. Bun and Deno run TS without a separate compile step.
install · init · runSetup
bash
# New project (Node)
npm init -y
npm install -D typescript @types/node
npx tsc --init # generates tsconfig.json
# Direct run without a build step
npm install -D tsx
npx tsx src/index.ts # ts-node replacement, ESM-friendly
# Type check only (no emit)
npx tsc --noEmit
# Format + lint
npm install -D eslint typescript-eslint prettier
npx eslint . --fix
# Bun / Deno — first-class TS support, no install needed
bun run src/index.ts
deno run src/index.ts
where things liveCommon imports
The only thing you import from TypeScript itself is types. Everything else — node,
zod, vitest — is JS that ships its types alongside.
import type { Foo } from "./foo"
Type-only import. Erased at runtime.
import { type Foo, bar } from "./foo"
Mixed: type modifier on the type member.
import { z } from "zod"
Runtime + types in one package.
import { describe, it, expect } from "vitest"
Default test runner.
import fs from "node:fs/promises"
node: prefix for stdlib.
import express, { type Request, type Response } from "express"
Library + its types from @types/express.
import path from "node:path"
CommonJS default-import works because of esModuleInterop.
type the basicsPrimitives & syntax
string · number · boolean · bigint · symbol
The five JS primitives.
null · undefined
Two flavours of “nothing”. strictNullChecks keeps them separate.
any
Avoid Opt-out of all checks. Almost never the right answer.
unknown
Preferred Top type. Must narrow before use.
never
Bottom type. Use for unreachable branches.
void
Function returns nothing meaningful.
readonly T[]
Immutable array view.
[string, number]
Fixed-length tuple.
[string, ...number[]]
Variadic tuple.
type Status = "ok" | "err"
Literal union.
enum Color { R, G, B }
Numeric enum. Prefer a const object + literal union for new code.
const COLORS = ["r","g","b"] as const
Locks to literal tuple. Source for derived unions.
type Color = typeof COLORS[number]
Derive a union from an array.
shape declarationstype vs interface
type T = { a: string }
Most flexible. Unions, intersections, mapped, conditional.
interface I { a: string }
Extendable. Declaration merging across files / libs.
interface I extends Base { … }
Single-parent. Pull more in with intersections.
type T = A & B
Intersection.
type T = A | B
Union.
type T = { readonly a: number; b?: string }
Read-only + optional fields.
type T = Record<string, number>
String-indexed map.
interface Window { __myFlag?: boolean }
Augment a 3rd-party type via merging.
Default to type for new code. Reach for interface
when you specifically want declaration merging (e.g. extending Express.Request).
type variablesGenerics
function id<T>(x: T): T
Type variable inferred from arg.
<T extends Bound>
Constrain the parameter.
<K extends keyof T>
Key parameter constrained to an object’s keys.
<T = string>
Default. Used when inference fails.
function f<const T>(x: T)
Const type parameter. Preserves literal narrowness.
type Map<T> = { [K in keyof T]: T[K] }
Mapped type.
type Map<T> = { [K in keyof T as `get${K}`]: T[K] }
Key remapping with template literals.
type R = T extends X ? Y : Z
Conditional type.
T extends (infer U)[] ? U : never
infer peels out a sub-type.
type Distrib<T> = T extends any ? T[] : never
Distributive conditional over unions.
type X = [T] extends […] ? …
Wrap in tuple to disable distribution.
typescript
// Generic function
function pluck(obj: T, key: K): T[K] {
return obj[key];
}
pluck({ name: "Ada", age: 36 }, "name"); // string
// Generic class with default
class Box {
constructor(public value: T) {}
}
// Conditional + infer — peel off the array element type
type ElementOf = T extends readonly (infer U)[] ? U : never;
type N = ElementOf; // number
// Mapped type with key remapping
type Getters = {
[K in keyof T as `get${Capitalize}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }
// Branded / nominal types via intersection
type UserId = string & { readonly __brand: "UserId" };
const asUserId = (s: string): UserId => s as UserId;
stdlib of typesUtility types
Partial<T>
All fields optional.
Required<T>
All fields required.
Readonly<T>
All fields readonly.
Pick<T, K>
Keep specific keys.
Omit<T, K>
Drop specific keys.
Record<K, V>
Object map with key + value types.
Exclude<U, X> / Extract<U, X>
Filter members of a union.
NonNullable<T>
Drop null / undefined.
ReturnType<typeof fn>
Extract return type of a function.
Parameters<typeof fn>
Tuple of param types.
Awaited<T>
Unwrap a Promise (recursively).
InstanceType<typeof Cls>
Type of new Cls(…).
Uppercase<S> / Lowercase<S> / Capitalize<S>
String literal transforms.
tell the compiler what you knowNarrowing
typeof x === "string"
Narrow primitives.
x instanceof Error
Narrow to a class.
"kind" in x
in guard narrows a union.
switch (x.kind) { case … }
Discriminated-union narrowing. The canonical pattern.
function isX(v): v is X
User-defined type guard.
function assertX(v): asserts v is X
Throw-on-failure narrowing.
x!
Non-null assertion. Use sparingly. Prefer real checks.
x as T
Cast. Bypass the checker; you own the lie.
satisfies T
Preferred over annotation. Verifies shape, keeps literal types.
x as const
Lock to literal tuple / object.
typescript
// Discriminated union — the canonical pattern
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "square": return s.side ** 2;
default: return assertNever(s); // exhaustive
}
}
function assertNever(x: never): never { throw new Error(`unreachable: ${x}`); }
// User-defined type guards
function isString(x: unknown): x is string {
return typeof x === "string";
}
// Assertion functions
function assertDefined(x: T | undefined): asserts x is T {
if (x === undefined) throw new Error("undefined");
}
// satisfies — keep literal types AND check shape
const config = {
port: 3000,
level: "info",
} satisfies { port: number; level: "info" | "warn" | "error" };
// type of config.level stays "info" (literal), not the union.
imports & declarationsModules
export const x = 1
Named export.
export default fn
Default export. Use sparingly — tree-shaking + DX cost.
export * from "./foo"
Re-export everything.
export { Foo as Bar } from "./foo"
Rename on re-export.
import type
Type-only import. Erased.
declare module "x" { … }
Type-only ambient module (no @types pkg).
declare global { interface Window { … } }
Augment globals.
/// <reference types="node" />
Triple-slash reference. Mostly legacy.
.d.ts files
Pure type definitions. Useful when shipping a library.
project knobstsconfig
"strict": true
Master flag. Turns on the canonical seven strictness checks.
"noUncheckedIndexedAccess": true
Highly recommended Array / record access returns T | undefined.
"exactOptionalPropertyTypes": true
Distinguish missing from undefined.
"target": "ES2022"
Output JS version. Match the runtime you ship to.
"module": "NodeNext"
Native ESM/CJS resolution for modern Node.
"moduleResolution": "Bundler"
Use with Vite / esbuild / Webpack toolchains.
"skipLibCheck": true
Skip checking .d.ts files. Always on for app code.
"isolatedModules": true
Compatibility with single-file transpilers (esbuild / swc).
"paths": { "@/*": ["src/*"] }
Path aliases. Mirror in the bundler config.
"composite": true + project references
Required for incremental multi-project builds (monorepos).
A tiny getJSON that takes a Zod schema and returns the inferred type —
static safety from TS, runtime safety from Zod. ~25 lines, no framework.
typescript
// Tiny typed HTTP client — generics + Zod for runtime validation.
import { z } from "zod";
const User = z.object({
id: z.number().int().positive(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer;
class HttpError extends Error {
constructor(public status: number, message: string) { super(message); }
}
async function getJSON(
url: string,
schema: S,
init?: RequestInit,
): Promise> {
const res = await fetch(url, init);
if (!res.ok) throw new HttpError(res.status, await res.text());
const data: unknown = await res.json();
const parsed = schema.safeParse(data);
if (!parsed.success) throw new Error(`Bad payload: ${parsed.error.message}`);
return parsed.data;
}
const u: User = await getJSON("https://api.example.com/users/42", User);
console.log(u.email); // type-safe + runtime-validated
Best practiceGood to know
Use satisfies, not annotation, when you want literal types.const x = {…} satisfies T checks the shape and preserves the
narrow literal types of each field — const x: T = {…} widens them.
Turn on noUncheckedIndexedAccess.
Catches the entire family of “array element is undefined” bugs at compile time. One of the highest
bug-per-letter strictness flags.
Validate untrusted input with Zod / Valibot, then trust the type.
TypeScript checks the shape you tell it about, not what arrives at runtime. Parse at the boundary; the
rest of the codebase stays clean.
Common trapsWatch out for
as is a lie.
It tells the checker to trust you. If the runtime shape differs, errors land far from the cast. Reserve
it for places where you genuinely know more than TS — library bridges, JSON boundary — and prefer
satisfies elsewhere.
Default exports cause renaming chaos.import x from "./a" picks whatever name it wants. Auto-imports drift,
refactors break silently. Prefer named exports.
Enums emit runtime code and don’t play with isolatedModules.
Use as const + typeof X[keyof typeof X]
instead. Same DX, zero JS output, friendlier to bundlers.
TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. It adds type annotations, interfaces, generics, and strict null checks to catch errors at compile time rather than at runtime. It is used in React and Vue frontends, Node.js backends, and monorepos.
What is the difference between type and interface in TypeScript?
Both define the shape of an object. interface is open — you can merge declarations across files with the same name. type can express unions, intersections, mapped types, conditional types, and template literal types that interfaces cannot. For simple object shapes, either works; prefer type when you need advanced type manipulation.
What are TypeScript utility types?
Utility types are built-in generic types that transform existing types. Partial<T> makes all properties optional; Required<T> makes all required; Pick<T, K> keeps only listed keys; Omit<T, K> drops listed keys; Record<K, V> builds an index type; Readonly<T> bans mutation; ReturnType<F> extracts a function return type.
What is type narrowing in TypeScript?
Narrowing is how TypeScript refines a union or broad type to a specific one inside a conditional block. Common narrowing guards are typeof (for primitives), instanceof (for classes), in (for property existence), and custom type predicates (x is Dog). TypeScript tracks these through control flow analysis.
Is TypeScript free to use?
Yes. TypeScript is open source and maintained by Microsoft under the Apache 2.0 license. It is free for personal and commercial use, and the TypeScript compiler (tsc) ships as an npm package at no cost.