This content originally appeared on DEV Community and was authored by Animesh Pandey
📑 Table of Contents
- Fundamentals
- Advanced Types
- Generics in Depth
- Type Inference & Compatibility
- Type Safety in JavaScript Interop
- Type Guards & Narrowing
- Advanced Patterns
- TypeScript in React
- Performance & Scaling
- Ecosystem & Future
🟦 Fundamentals (Expert Level)
1. 🧩 Type System vs Runtime Behavior
Definition:
TypeScript is a compile-time type system — all types are erased at runtime.
✅ Key Points
- Types are structural, not nominal (duck typing).
- Type checking = compile-time only.
- At runtime, it’s just JavaScript.
⚠️ Gotchas
- Type safety doesn’t prevent runtime errors:
function greet(name: string) {
return "Hello " + name.toUpperCase();
}
greet((123 as unknown) as string); // compiles, but runtime crash
- No runtime enforcement — you need runtime validators (
zod
,io-ts
,yup
).
🎯 Interview One-Liner
“TypeScript’s types disappear at runtime. They make dev-time guarantees, but runtime safety requires explicit validation.”
2. 🔢 Core Primitive Types
✅ Basic Types
-
string
,number
,boolean
-
null
,undefined
-
symbol
,bigint
✅ Special Types
-
any
→ Opt-out of type system (unsafe). -
unknown
→ Safer alternative; must narrow before use. -
never
→ Function that never returns (errors, infinite loops). -
void
→ Function returns nothing.
⚠️ Gotchas
let x: any = 42;
x.toUpperCase(); // compiles, runtime crash
let y: unknown = 42;
y.toUpperCase(); // ❌ compile error
🎯 Interview One-Liner
“
any
disables safety. Preferunknown
when you don’t know the type, since it forces narrowing before use.”
3. 🏷️ Type Assertions vs Casting
Definition:
Tell the compiler “trust me, this is of type X.”
✅ Syntax
const el = document.getElementById("foo") as HTMLDivElement;
✅ Non-null Assertion
const el = document.getElementById("foo")!; // never null
⚠️ Gotchas
- Overusing assertions bypasses safety.
- Wrong assertions → runtime crashes.
- Non-null
!
can hide real null bugs.
🎯 Interview One-Liner
“Assertions tell TS to trust you. They don’t change runtime — overuse can hide real errors.”
4. 🔍 Type Narrowing (Control Flow Analysis)
Definition:
TS refines types based on runtime checks.
✅ Techniques
typeof
instanceof
- Equality checks
- Discriminated unions (tag property)
Example
function printLen(x: string | string[]) {
if (typeof x === "string") console.log(x.length);
else console.log(x.length); // string[] length
}
⚠️ Gotchas
- Narrowing only works if TS can see the check.
- External function calls won’t narrow unless you define type predicates.
function isString(x: unknown): x is string {
return typeof x === "string";
}
🎯 Interview One-Liner
“TypeScript narrows unions using control flow. You can extend it with custom type predicates.”
5. 🚦 Strict Null Checking
Definition:
With strictNullChecks
, null
and undefined
are not assignable to other types unless explicitly included.
✅ Example
let x: string = null; // ❌ error under strictNullChecks
let y: string | null = null; // ✅
✅ Optional Chaining + Nullish Coalescing
const name = user?.profile?.name ?? "Guest";
⚠️ Gotchas
- Many JS libs don’t account for
undefined
. - Without
strictNullChecks
, you get unsound behavior (nullable everywhere).
🎯 Interview One-Liner
“Strict null checks make nullability explicit, avoiding the billion-dollar mistake. Combine with optional chaining and nullish coalescing.”
6. 🔎 Structural Typing (vs Nominal)
Definition:
TS uses structural typing — compatibility is based on shape, not declared type.
✅ Example
type Point = { x: number; y: number };
type Coord = { x: number; y: number };
let a: Point = { x: 1, y: 2 };
let b: Coord = a; // ✅ works (same shape)
⚠️ Gotchas
- Extra properties are rejected in object literals:
let p: Point = { x: 1, y: 2, z: 3 }; // ❌ error
- But extra props are allowed if passed as variable:
const tmp = { x: 1, y: 2, z: 3 };
let p: Point = tmp; // ✅ works
🎯 Interview One-Liner
“TypeScript is structurally typed — if it quacks like a duck, it’s assignable. But object literals have stricter excess property checks.”
7. 📝 Type Aliases vs Interfaces
Definition:
Two ways to define object types.
✅ Differences
- Interface: extendable via declaration merging.
- Type Alias: supports unions, intersections, primitives.
Example
interface A { x: number }
interface A { y: number } // merged
type B = { x: number } & { y: number } // no merging, must intersect
⚠️ Gotchas
- Prefer
interface
for public APIs (extendable). - Prefer
type
for unions and complex compositions.
🎯 Interview One-Liner
“Interfaces are open (mergeable), types are closed but more flexible (unions, intersections). Use each where it fits.”
8. 📦 Enums vs Literal Types
✅ Enums
enum Direction { Up, Down }
✅ Literal Unions
type Direction = "up" | "down";
⚠️ Gotchas
- String literal unions are usually better (type-safe, no runtime overhead).
- Enums generate runtime objects → heavier.
-
const enum
is inlined at compile time but can break tooling.
🎯 Interview One-Liner
“Prefer literal unions over enums — they’re lighter and more type-safe. Use enums only when runtime mapping is required.”
🟦 Advanced Types
1. ➕ Union & Intersection Types
✅ Union (|
)
- Represents either type.
type Input = string | number;
let val: Input = "hi"; // ✅
val = 42; // ✅
✅ Intersection (&
)
- Combines multiple types.
type Person = { name: string };
type Worker = { company: string };
type Employee = Person & Worker;
// { name: string; company: string }
⚠️ Gotchas
- Union narrows to shared members:
function len(x: string | string[]) {
return x.length; // works (length exists on both)
}
- Intersection of incompatible types →
never
:
type Impossible = string & number; // never
🎯 One-Liner
“Unions = OR, intersections = AND. Incompatible intersections collapse to never.”
2. 🔢 Literal Types & Const Assertions
✅ Literal Types
let dir: "up" | "down";
dir = "up"; // ✅
dir = "left"; // ❌
✅ as const
- Locks values into literal types.
const colors = ["red", "green"] as const;
type Color = typeof colors[number]; // "red" | "green"
🎯 One-Liner
“Literal types restrict values to exact strings/numbers.
as const
freezes arrays/objects for inference.”
3. 📝 Template Literal Types
✅ Key Points
- Build strings at type level.
type Event = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"
✅ Use Cases
- Event handler names.
- Typed CSS props.
- API route patterns.
🎯 One-Liner
“Template literal types compose strings at type level — great for event names, routes, and API typings.”
4. 🧮 Conditional Types
Definition:
T extends U ? X : Y
✅ Example
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<number>; // false
✅ Distributive Behavior
type ToArray<T> = T extends any ? T[] : never;
type C = ToArray<string | number>; // string[] | number[]
⚠️ Gotchas
- Distribution only happens on naked type parameters.
- Use brackets to disable:
type NoDistrib<T> = [T] extends [any] ? T[] : never;
🎯 One-Liner
“Conditional types let you branch at type level. By default they distribute over unions.”
5. 🗂️ Mapped Types
✅ Basics
type OptionsFlags<T> = {
[K in keyof T]: boolean;
};
type Features = { darkMode: () => void };
type Flags = OptionsFlags<Features>;
// { darkMode: boolean }
✅ Modifiers
readonly
-
?
optional -
-readonly
/-?
to remove
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
🎯 One-Liner
“Mapped types iterate over keys to transform shape — add/remove optionality, readonly, etc.”
6. 🛠️ Utility Types
✅ Built-ins
-
Partial<T>
→ all optional -
Required<T>
→ all required -
Readonly<T>
→ all readonly -
Pick<T, K>
→ subset -
Omit<T, K>
→ all except -
Record<K, V>
→ dict type -
ReturnType<T>
→ infer fn return -
Parameters<T>
→ tuple of args -
InstanceType<T>
→ type ofnew T()
Example
type Todo = { id: number; title: string; done?: boolean };
type TodoDraft = Partial<Todo>; // all optional
🎯 One-Liner
“Utility types like Partial, Pick, Omit, Record abstract common transformations of object types.”
7. 🧩 Key Remapping in Mapped Types (TS 4.1+)
type Prefix<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: T[K]
};
type Events = Prefix<{ click: () => void }>;
// { onClick: () => void }
🎯 One-Liner
“Mapped types can rename keys dynamically using
as
clauses and template literals.”
8. 🌀 Recursive & Deep Types
✅ Recursive Types
type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };
✅ Deep Utility
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
⚠️ Gotchas
- Deep recursion can slow compiler dramatically.
- Use cautiously in very large codebases.
🎯 One-Liner
“Recursive types enable JSON-like structures and deep utilities, but may hit compiler perf limits.”
🟦 Generics in Depth
1. 🧩 Generic Functions
Definition:
Functions parameterized with type variables.
✅ Example
function identity<T>(arg: T): T {
return arg;
}
const a = identity<string>("hello");
const b = identity(42); // inferred as number
🎯 One-Liner
“Generics make functions reusable across types, with inference for convenience.”
2. 🔒 Constraints (extends
)
Restrict generics to a subset of types.
✅ Example
function getLength<T extends { length: number }>(x: T) {
return x.length;
}
getLength("hi"); // ✅
getLength([1,2,3]); // ✅
getLength(42); // ❌
🎯 One-Liner
“Constraints limit generics with
extends
, allowing access to known members.”
3. 🏷️ Generic Interfaces & Classes
✅ Interfaces
interface Box<T> { value: T }
const stringBox: Box<string> = { value: "hi" };
✅ Classes
class Container<T> {
constructor(public value: T) {}
}
const c = new Container<number>(123);
🎯 One-Liner
“Generics extend beyond functions — interfaces and classes can also be parameterized.”
4. ⚖️ Default Generic Parameters
Provide defaults for flexibility.
✅ Example
interface ApiResponse<T = any> {
data: T;
error?: string;
}
const res: ApiResponse = { data: "hi" }; // T defaults to any
🎯 One-Liner
“Defaults reduce boilerplate when generic type can be inferred or safely assumed.”
5. 🔄 Keyof & Indexed Access with Generics
✅ Keyof
type Keys<T> = keyof T;
type User = { id: number; name: string };
type UKeys = Keys<User>; // "id" | "name"
✅ Indexed Access
type Value<T, K extends keyof T> = T[K];
type NameType = Value<User, "name">; // string
🎯 One-Liner
“Keyof + indexed access lets you make type-safe utilities (like Pick/Omit).”
6. 🎭 Conditional Generics
Generics inside conditional types = super powerful.
✅ Example
type Flatten<T> = T extends any[] ? T[number] : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number
🎯 One-Liner
“Conditional generics let you branch inside generics — e.g., flatten arrays, unwrap promises.”
7. ⚖️ Variance (Covariance vs Contravariance)
Definition:
How subtyping interacts with generics.
✅ Covariance
- Safe to use subtype in place of supertype.
let str: string = "hi";
let val: string | number = str; // ✅
✅ Contravariance
- Function parameters are contravariant:
type Fn<T> = (x: T) => void;
let fn: Fn<string | number> = (x: string) => {}; // ✅
⚠️ Gotchas
- TypeScript uses bivariant function parameters by default for compatibility (unsafe but practical).
-
--strictFunctionTypes
enforces contravariance.
🎯 One-Liner
“Function parameters are contravariant, return types are covariant. TS is bivariant by default unless strictFunctionTypes is on.”
8. 🌀 Higher-Kinded Types (HKTs, Workarounds)
TypeScript doesn’t support true HKTs (types parameterized over type constructors), but you can simulate.
✅ Example
interface Functor<F> {
map<A, B>(fa: F & { value: A }, fn: (a: A) => B): F & { value: B }
}
Or libraries like fp-ts
emulate HKT with encoding tricks.
🎯 One-Liner
“TypeScript lacks HKTs, but libraries like fp-ts emulate them with encoding patterns.”
9. 🔗 Generics in React
✅ Typing useState
const [val, setVal] = useState<string | null>(null);
✅ Typing useReducer
type Action = { type: "inc" } | { type: "dec" };
function reducer(s: number, a: Action): number {
return a.type === "inc" ? s+1 : s-1;
}
const [count, dispatch] = useReducer(reducer, 0);
✅ Typing Props
interface ButtonProps<T extends "button" | "a"> {
as: T;
props: T extends "a" ? { href: string } : { onClick: () => void };
}
🎯 One-Liner
“Generics in React type hooks, props, reducers, and flexible components (e.g., polymorphic components).”
🟦 Type Inference & Compatibility
1. 🔍 Type Inference Basics
Definition:
TypeScript infers types when not explicitly annotated.
✅ Examples
let x = 42; // inferred as number
let y = [1, 2]; // inferred as number[]
✅ Contextual Typing
- Function parameters infer type from usage:
window.onmousedown = (e) => {
console.log(e.button); // e inferred as MouseEvent
};
🎯 One-Liner
“Type inference works both from initializer values and from context (like callbacks).”
2. ⚖️ Excess Property Checks
Definition:
TypeScript enforces stricter checks for object literals.
✅ Example
type User = { name: string };
const u1: User = { name: "Alice", age: 30 }; // ❌ excess property
const tmp = { name: "Alice", age: 30 };
const u2: User = tmp; // ✅ allowed (assignment, not literal)
⚠️ Gotchas
- Literal checks prevent typos in inline objects.
- Assigning via variable bypasses check.
🎯 One-Liner
“Object literals get extra checks for unknown properties. Assign via variable to bypass.”
3. 🔄 Type Widening & Narrowing
✅ Widening
- Without
as const
, literal types widen:
let a = "hi"; // type string (widened)
const b = "hi"; // type "hi" (literal)
✅ Narrowing
- Control-flow analysis narrows union types:
function f(x: string | null) {
if (x !== null) return x.toUpperCase(); // narrowed to string
}
⚠️ Gotchas
-
let x = null
→ type isany
unless strictNullChecks. - Arrays widen unless frozen with
as const
.
🎯 One-Liner
“TS widens literals by default. Use
as const
to keep narrow literal types.”
4. 🏷️ Assignability & Compatibility
Definition:
TypeScript is structurally typed → assignability depends on shape.
✅ Example
type Point = { x: number; y: number };
type Coord = { x: number; y: number; z?: number };
let p: Point = { x: 1, y: 2 };
let c: Coord = p; // ✅ works
p = c; // ✅ works (z optional)
⚠️ Gotchas
- Function parameters are bivariant by default (unsafe).
- Use
strictFunctionTypes
for true contravariance.
🎯 One-Liner
“TS uses structural typing: if shapes match, types are compatible. Functions default to bivariant params unless strict.”
5. 🧩 any
vs unknown
vs never
✅ any
- Opt-out of type checking.
- Can be assigned to/from anything.
let a: any = 42;
a.foo.bar(); // compiles, runtime error
✅ unknown
- Top type (safe any).
- Must be narrowed before use.
let b: unknown = 42;
b.toUpperCase(); // ❌ error
if (typeof b === "string") b.toUpperCase(); // ✅
✅ never
- Bottom type (no value possible).
- Used in exhaustiveness checks.
type Shape = { kind: "circle" } | { kind: "square" };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return 1;
case "square": return 2;
default: const _exhaustive: never = s; // ✅ ensures all cases handled
}
}
🎯 One-Liner
“
any
disables safety,unknown
forces narrowing,never
represents impossible cases.”
6. 🧠 Inference in Functions
✅ Return Inference
function add(a: number, b: number) {
return a + b; // inferred as number
}
✅ Generic Inference
function first<T>(arr: T[]): T {
return arr[0];
}
const v = first(["a", "b"]); // v: string
⚠️ Gotchas
- Sometimes inference is too wide:
function f() { return Math.random() ? "a" : "b"; }
// inferred as string, not "a"|"b"
→ Fix with as const
or explicit typing.
🎯 One-Liner
“Function return types are inferred, but unions may widen. Use
as const
or annotations for precision.”
7. 🛠️ Type Compatibility in Enums
✅ Numeric Enums
- Compatible with numbers.
enum Dir { Up, Down }
let d: Dir = 0; // ✅
✅ String Enums
- Not compatible with strings unless explicitly.
enum Color { Red = "red" }
let c: Color = "red"; // ❌
🎯 One-Liner
“Numeric enums are assignable to numbers, but string enums require exact matches.”
8. ⚡ Type Compatibility in Tuples vs Arrays
✅ Tuples
type Pair = [string, number];
let p: Pair = ["hi", 42];
- Tuples have fixed length, arrays are flexible.
⚠️ Gotchas
let t: [number, number] = [1, 2];
t.push(3); // ✅ allowed! TS doesn’t enforce length at runtime
🎯 One-Liner
“Tuples enforce order but not length at runtime — they’re arrays under the hood.”
🟦 Type Safety in JavaScript Interop
1. 📜 Ambient Declarations (declare
)
Definition:
Tell TypeScript about variables/modules that exist at runtime (JS), but aren’t defined in TS code.
✅ Examples
declare const VERSION: string;
console.log(VERSION); // TS knows VERSION is string
declare module "legacy-lib" {
export function legacyFn(): void;
}
⚠️ Gotchas
- Declarations don’t generate runtime code — only inform compiler.
- If declaration doesn’t match actual runtime → runtime errors.
🎯 One-Liner
“
declare
informs the type system about external JS values but doesn’t emit code. Accuracy is critical or you’ll get runtime errors.”
2. 📦 Type Declarations for JS Libraries
✅ Strategies
- @types packages:
npm install --save-dev @types/lodash
-
Manual
d.ts
files for missing types:
// lodash.d.ts
declare module "lodash" {
export function chunk<T>(arr: T[], size: number): T[][];
}
⚠️ Gotchas
- If types are outdated/mismatched → TS lies about API.
- Can “augment” instead of redefining (see below).
🎯 One-Liner
“Missing types for JS libs? Install
@types
or write.d.ts
files — but keep them accurate with the runtime.”
3. 🧩 Module Augmentation & Declaration Merging
Definition:
Extend existing module or type definitions without rewriting.
✅ Example
// add a method to lodash
declare module "lodash" {
export function customHello(): string;
}
✅ Declaration Merging
- Interfaces with the same name merge:
interface User { id: number }
interface User { name: string }
const u: User = { id: 1, name: "Alice" }; // ✅
⚠️ Gotchas
- Augmentation is global → can cause conflicts across packages.
- Prefer module augmentation for libs, not
any
hacks.
🎯 One-Liner
“Declaration merging/augmentation extends types safely. Useful for adding custom fields or extending 3rd-party libraries.”
4. 🎯 as const
for Safe Interop
Definition:
as const
freezes literals into readonly narrow types.
✅ Example
const roles = ["admin", "user", "guest"] as const;
type Role = typeof roles[number];
// "admin" | "user" | "guest"
✅ Use Case
- Ensures config/constants match at runtime.
- Great for enums/union types from JS arrays.
🎯 One-Liner
“
as const
locks JS literals into readonly narrow types — perfect for role lists, config, or enum-like structures.”
5. ⚙️ tsconfig
Strictness Flags
✅ Important Flags
-
strict
→ enables all strict checks. -
noImplicitAny
→ no silentany
. -
strictNullChecks
→ enforce explicit nullability. -
noUncheckedIndexedAccess
→ array lookups can returnundefined
. -
exactOptionalPropertyTypes
→?
means strictly optional.
⚠️ Gotchas
- Teams often disable strictness → leaks
any
. - Migrating large JS → TS requires incremental adoption.
🎯 One-Liner
“Always enable
strict
. Key flags likenoImplicitAny
andstrictNullChecks
catch hidden bugs in JS interop.”
6. 🛠️ Working with Untyped JS Objects
✅ Options
- Use
unknown
+ type guards:
function isUser(u: any): u is { id: number } {
return typeof u.id === "number";
}
- Or validate at runtime with Zod/io-ts:
import { z } from "zod";
const User = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof User>;
🎯 One-Liner
“Untyped JS objects should be validated at runtime with schemas (Zod/io-ts), not just trusted via
any
.”
7. 🔗 Migrating JS → TS (Gradual Typing)
✅ Strategies
- Rename
.js
→.ts
or.tsx
. - Use
allowJs
+checkJs
in tsconfig to gradually type JS. - Add JSDoc annotations:
/**
* @param {string} name
* @returns {string}
*/
function greet(name) { return "Hello " + name; }
- TS infers types from JSDoc until converted.
⚠️ Gotchas
- JSDoc typing is weaker than TS proper.
- Incremental migration often mixes
any
→ must clean up later.
🎯 One-Liner
“Migrate JS → TS gradually: enable
checkJs
, add JSDoc types, then refactor into full TypeScript.”
8. 🛡️ Runtime vs Compile-Time Safety
Core Rule:
Types vanish at runtime → if interoping with JS, you need runtime checks.
✅ Example
function safeParse(json: string): unknown {
try { return JSON.parse(json) } catch { return null }
}
const data = safeParse("not-json"); // type unknown
// must validate before using
🎯 One-Liner
“TypeScript only checks at compile time. For JS interop, add runtime validation to truly guarantee safety.”
🟦 Type Guards & Narrowing
1. 🔎 Built-in Type Guards: typeof
Definition:
typeof
lets TypeScript narrow primitive types.
✅ Example
function log(val: string | number) {
if (typeof val === "string") {
console.log(val.toUpperCase()); // val: string
} else {
console.log(val.toFixed(2)); // val: number
}
}
⚠️ Gotchas
- Only works on primitives:
"string" | "number" | "boolean" | "bigint" | "symbol" | "undefined" | "object" | "function"
. - Doesn’t differentiate
null
vsobject
(typeof null === "object"
).
🎯 One-Liner
“Use
typeof
for primitives — but notetypeof null === 'object'
.”
2. 🏗️ Built-in Type Guards: instanceof
Definition:
Narrow objects by checking prototype chain.
✅ Example
function handleError(e: Error | string) {
if (e instanceof Error) {
console.error(e.message); // Error
} else {
console.error(e); // string
}
}
⚠️ Gotchas
- Only works with classes/constructors (not plain objects).
- Inheritance hierarchy is respected.
🎯 One-Liner
“Use
instanceof
for class-based narrowing — it checks prototype chain.”
3. 🧩 In-Operator Narrowing
Definition:
Check if a property exists in object → narrows union.
✅ Example
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(animal: Cat | Dog) {
if ("meow" in animal) animal.meow();
else animal.bark();
}
🎯 One-Liner
“The
in
operator narrows unions by checking for property existence.”
4. 🏷️ Discriminated (Tagged) Unions
Definition:
Unions with a common literal field (“tag”) for safe narrowing.
✅ Example
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape) {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "square": return s.side ** 2;
}
}
🎯 One-Liner
“Discriminated unions use a common literal field to guarantee safe narrowing in switches.”
5. 🛠️ Custom Type Predicates
Definition:
User-defined functions that tell TS a condition implies a type.
✅ Example
function isString(x: unknown): x is string {
return typeof x === "string";
}
function log(x: unknown) {
if (isString(x)) console.log(x.toUpperCase());
}
🎯 One-Liner
“Custom type predicates (
x is T
) let you teach TS how to narrow beyond built-ins.”
6. 🚦 Exhaustiveness Checking with never
Definition:
Force handling all cases in a union.
✅ Example
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; s: number };
function perimeter(s: Shape) {
switch (s.kind) {
case "circle": return 2 * Math.PI * s.r;
case "square": return 4 * s.s;
default:
const _exhaustive: never = s; // compile error if new case added
return _exhaustive;
}
}
🎯 One-Liner
“Exhaustiveness checks with
never
ensure all union cases are handled — future-proofing code.”
7. 🧠 Control Flow Analysis
Definition:
TS tracks variables through branches to narrow automatically.
✅ Example
function f(x: string | null) {
if (!x) return;
// x is now string, since null was filtered out
return x.toUpperCase();
}
⚠️ Gotchas
- TS is flow-sensitive — order matters.
- Reassignments can widen again.
🎯 One-Liner
“TS narrows types flow-sensitively — once a check passes, TS refines type until reassignment.”
8. 🧩 Assertion Functions (TS 3.7+)
Definition:
Custom functions that throw on invalid values while narrowing type.
✅ Example
function assertIsString(x: any): asserts x is string {
if (typeof x !== "string") throw new Error("Not a string");
}
function shout(x: any) {
assertIsString(x);
console.log(x.toUpperCase()); // x: string
}
🎯 One-Liner
“Assertion functions throw at runtime and tell TS the variable is narrowed if no error occurs.”
9. 📦 Combining Guards
✅ Example
function handle(input: string | number | null) {
if (input == null) return; // null/undefined filtered
if (typeof input === "string") { // narrowed to string
return input.toUpperCase();
}
return input.toFixed(2); // number
}
🎯 One-Liner
“Combine guards (
== null
,typeof
,instanceof
) for precise narrowing of complex unions.”
🟦 Advanced Patterns
1. 🏷️ Branded Types (Nominal Typing in TS)
Problem:
TS is structural — type UserId = string
is indistinguishable from any string
.
Solution:
Add a “brand” field to enforce nominal typing.
✅ Example
type UserId = string & { __brand: "UserId" };
function getUser(id: UserId) {}
getUser("123" as UserId); // ✅
getUser("random"); // ❌ must be branded
🎯 One-Liner
“Branded types simulate nominal typing in TS, preventing accidental mixing of structurally identical types.”
2. 🌀 Opaque Types
Definition:
Similar to branded types, but completely hide the underlying type from consumers.
✅ Example
type Opaque<K, T> = T & { __TYPE__: K };
type UserId = Opaque<"UserId", string>;
function createUserId(s: string): UserId {
return s as UserId;
}
🎯 One-Liner
“Opaque types hide implementation details and prevent misuse, forcing controlled constructors.”
3. 🛠️ Recursive Utility Types
✅ DeepPartial
type DeepPartial<T> = {
[K in keyof T]?: DeepPartial<T[K]>;
};
✅ DeepReadonly
type DeepReadonly<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
};
⚠️ Gotchas
- Deep recursion can slow down compiler.
- Use selectively in large projects.
🎯 One-Liner
“Recursive types enable deep utilities like DeepPartial, but heavy use impacts compiler performance.”
4. 🧩 Conditional & Mapped Utilities
✅ NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;
✅ Diff
type Diff<T, U> = T extends U ? never : T;
✅ Overwrite
type Overwrite<T, U> = Omit<T, keyof U> & U;
🎯 One-Liner
“Mapped + conditional types let you build powerful utilities like Diff, Overwrite, NonNullable.”
5. 🔄 Variadic Tuple Types
Definition:
Model tuples of variable length with generics.
✅ Example
type Push<T extends any[], V> = [...T, V];
type T1 = Push<[1,2], 3>; // [1,2,3]
type Concat<T extends any[], U extends any[]> = [...T, ...U];
type T2 = Concat<[1,2], [3,4]>; // [1,2,3,4]
🎯 One-Liner
“Variadic tuple types let you append, prepend, or merge tuples while preserving type precision.”
6. 🏗️ Builder Pattern in TypeScript
✅ Example
class RequestBuilder {
private url: string = "";
private method: "GET" | "POST" = "GET";
setUrl(url: string) { this.url = url; return this; }
setMethod(m: "GET" | "POST") { this.method = m; return this; }
build() { return { url: this.url, method: this.method }; }
}
const req = new RequestBuilder().setUrl("/api").setMethod("POST").build();
🎯 One-Liner
“Builder patterns ensure chained configuration APIs with type safety and autocomplete.”
7. 🛡️ Exact Types (Prevent Excess Keys)
Problem:
TS normally allows extra props via assignment.
Solution:
Create an Exact<T, U>
utility.
✅ Example
type Exact<T, U extends T> = T & { [K in Exclude<keyof U, keyof T>]?: never };
type Person = { name: string };
const p: Exact<Person, { name: string; age: number }> = { name: "A", age: 20 };
// ❌ error: extra 'age'
🎯 One-Liner
“Exact types prevent excess keys, useful for APIs where only known fields are allowed.”
8. 📦 Extracting Types from Values
✅ typeof + keyof
const config = {
roles: ["admin", "user", "guest"] as const,
};
type Role = typeof config["roles"][number];
// "admin" | "user" | "guest"
🎯 One-Liner
“Use
typeof
andas const
to derive union types from runtime values like config arrays.”
9. 🧠 Phantom Types (Static Guarantees)
Definition:
Types that exist only at compile-time, to encode invariants.
✅ Example
type Celsius = number & { __unit: "Celsius" };
type Fahrenheit = number & { __unit: "Fahrenheit" };
function toF(c: Celsius): Fahrenheit { return (c * 9/5 + 32) as Fahrenheit; }
let t: Celsius = 100 as Celsius;
toF(t); // ✅
toF(100 as Fahrenheit); // ❌
🎯 One-Liner
“Phantom types enforce domain-specific rules (like units) without runtime cost.”
🟦 TypeScript in React
1. 🎯 Typing Component Props
✅ Functional Components
type ButtonProps = {
label: string;
onClick?: () => void;
};
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
⚠️ Gotchas
-
React.FC
implicitly addschildren
prop — often unwanted. - Better to type
children
explicitly.
type Props = { children?: React.ReactNode };
🎯 One-Liner
“Prefer explicit prop typing over
React.FC
to avoid hiddenchildren
.”
2. 🏷️ Children Typing
✅ Common Patterns
type Props = { children: React.ReactNode }; // anything renderable
type Props2 = { children: React.ReactElement }; // exactly one element
type Props3<T> = { children: (data: T) => React.ReactNode }; // render prop
🎯 One-Liner
“Use
ReactNode
for generic children,ReactElement
for single elements, and functions for render props.”
3. 🧩 Typing Hooks (useState
, useReducer
)
✅ useState
const [count, setCount] = useState<number>(0); // explicit
const [name, setName] = useState("Alice"); // inferred as string
-
useState<T | null>(null)
when initial value is null.
✅ useReducer
type Action = { type: "inc" } | { type: "dec" };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "inc": return state + 1;
case "dec": return state - 1;
}
}
const [count, dispatch] = useReducer(reducer, 0);
🎯 One-Liner
“Type state & actions explicitly in hooks. Use union types for reducers.”
4. 🌐 Typing Context Providers
✅ Example
type User = { id: string; name: string };
type UserContextType = { user: User | null; setUser: (u: User) => void };
const UserContext = React.createContext<UserContextType | undefined>(undefined);
function useUser() {
const ctx = React.useContext(UserContext);
if (!ctx) throw new Error("useUser must be inside UserProvider");
return ctx;
}
🎯 One-Liner
“Context values should include both data and setters, wrapped in a custom hook for safety.”
5. 🧵 Typing Refs & forwardRef
✅ DOM Refs
const inputRef = React.useRef<HTMLInputElement>(null);
inputRef.current?.focus();
✅ forwardRef
type InputProps = { label: string };
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label }, ref) => <input ref={ref} placeholder={label} />
);
✅ useImperativeHandle
type Handle = { focus: () => void };
const CustomInput = React.forwardRef<Handle>((props, ref) => {
const inputRef = React.useRef<HTMLInputElement>(null);
React.useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
}));
return <input ref={inputRef} />;
});
🎯 One-Liner
“Use
forwardRef
with generic ref types. Expose imperative APIs withuseImperativeHandle
.”
6. 🧮 Typing Higher-Order Components (HOCs)
✅ Example
function withLoading<T>(Component: React.ComponentType<T>) {
return (props: T & { loading: boolean }) =>
props.loading ? <div>Loading...</div> : <Component {...props} />;
}
⚠️ Gotchas
- Must preserve props (
T
) and merge with new ones. - Watch out for lost generics when wrapping.
🎯 One-Liner
“HOCs should preserve original props via generics and merge additional ones.”
7. 🧑🔬 Typing Generic Components
✅ Example
type ListProps<T> = {
items: T[];
render: (item: T) => React.ReactNode;
};
function List<T>({ items, render }: ListProps<T>) {
return <ul>{items.map(render)}</ul>;
}
<List items={[1, 2, 3]} render={(x) => <li>{x}</li>} />;
🎯 One-Liner
“Generic components let props depend on type parameters — perfect for reusable lists and tables.”
8. 🔄 Typing Event Handlers
✅ Example
function Form() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return <input onChange={handleChange} />;
}
✅ Common Event Types
React.MouseEvent<HTMLButtonElement>
React.ChangeEvent<HTMLInputElement>
React.FormEvent<HTMLFormElement>
🎯 One-Liner
“Use React’s synthetic event types (MouseEvent, ChangeEvent) for handlers, parameterized by element type.”
9. 🧩 Typing Polymorphic as
Components
✅ Example
type PolymorphicProps<T extends React.ElementType> = {
as?: T;
children: React.ReactNode;
} & React.ComponentProps<T>;
function Box<T extends React.ElementType = "div">({ as, ...props }: PolymorphicProps<T>) {
const Component = as || "div";
return <Component {...props} />;
}
<Box as="a" href="https://ts">Link</Box>; // href type-safe
🎯 One-Liner
“Polymorphic components use
as
+ generics +ComponentProps<T>
to forward correct props.”
🟦 Performance & Scaling TypeScript
1. ⚡ Type-Checking Performance in Large Projects
✅ Common Bottlenecks
- Deeply nested conditional types.
- Overuse of recursive mapped types (e.g., DeepPartial, DeepReadonly).
- Giant union types (e.g.,
"A" | "B" | ... | "Z"
with hundreds of members). - Heavy
infer
usage inside generics.
✅ Tools
-
tsc --diagnostics
→ measure type-check performance. -
tsc --extendedDiagnostics
→ detailed breakdown (parse time, check time, emit time).
🎯 One-Liner
“The biggest type-check killers are deep recursion, massive unions, and heavy conditional types — profile with
--diagnostics
.”
2. 🛠️ Avoiding Over-Complex Types
✅ Problem
Some teams abuse TS to encode too much at type level.
type Crazy<T> = T extends string
? { str: T }
: T extends number
? { num: T }
: never;
- Hard to maintain, slows compiler.
✅ Guidelines
- Keep types simple for DX (developer experience).
- Don’t encode logic that belongs in runtime code.
- Prefer branded/opaque types for safety, instead of extreme conditional gymnastics.
🎯 One-Liner
“Don’t over-engineer types — TS is for safety, not replacing runtime logic.”
3. 📦 Build Pipelines: tsc
vs Babel vs SWC
✅ tsc
- Full type-checker + emit.
- Slowest, but canonical.
✅ Babel with @babel/preset-typescript
- Strips types → no type-checking.
- Fast, but must run
tsc --noEmit
separately.
✅ SWC (used in Next.js, Vite, Turborepo)
- Rust-based transpiler, very fast.
- Strips types only.
⚠️ Gotchas
- Babel/SWC do not catch type errors — must run type-check separately in CI.
🎯 One-Liner
“Use SWC/Babel for fast builds, but keep
tsc --noEmit
in CI for type safety.”
4. 🔄 Incremental Compilation
✅ Options
-
"incremental": true
intsconfig.json
→ saves.tsbuildinfo
cache. -
"composite": true
for project references (multi-package repos).
✅ Benefits
- Only re-check changed files.
- Required for monorepos with shared libraries.
🎯 One-Liner
“Enable
incremental
+composite
in tsconfig to avoid full re-checks in large repos.”
5. 🧩 Project References (Scaling Monorepos)
Definition:
Break project into multiple sub-projects with clear build boundaries.
✅ Example
// tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/ui" },
{ "path": "./packages/server" }
]
}
✅ Benefits
- Faster builds (independent packages).
- Enforces dependency contracts.
- Works great with Nx/Turborepo.
🎯 One-Liner
“Project references let you split large codebases into smaller typed units with enforced contracts.”
6. 🧠 Type-Only Imports & Exports (TS 3.8+)
✅ Example
import type { User } from "./types"; // stripped at runtime
export type { Config } from "./config";
✅ Benefits
- Avoids accidentally bundling type-only modules.
- Reduces unnecessary runtime imports.
🎯 One-Liner
“Use
import type
andexport type
to ensure pure type imports don’t affect runtime bundles.”
7. ⚖️ Managing any
at Scale
✅ Problems
-
any
spreads like a virus in codebases. - One
any
can propagate through dozens of types.
✅ Solutions
- Use
unknown
instead ofany
when possible. - Use
eslint
rules (@typescript-eslint/no-explicit-any
). - Introduce “escape hatches” (
TODO: fix any
) but track debt.
🎯 One-Liner
“Manage
any
aggressively — preferunknown
, enforce lint rules, and track escape hatches.”
8. 📊 Large Codebase Best Practices
- ✅ Strict mode always (
strict: true
). - ✅ Type-only imports (
import type
). - ✅ Use Zod/io-ts for runtime validation of API responses.
- ✅ Add
tsc --noEmit
in CI to enforce type safety. - ✅ Use
paths
intsconfig.json
for clean imports. - ✅ Monitor
tsc --diagnostics
to spot type-check slowdowns.
🎯 One-Liner
“Large TS projects succeed when strict mode, type-only imports, runtime validation, and CI type-checks are enforced.”
🟦 Ecosystem & Future
1. 🧩 Decorators (Stage 3 Proposal)
Definition:
Annotations for classes, methods, and properties.
✅ Example
function readonly(target: any, key: string) {
Object.defineProperty(target, key, { writable: false });
}
class User {
@readonly
name = "Alice";
}
✅ Use Cases
- Dependency injection (NestJS).
- ORMs (TypeORM, Prisma).
- Metadata reflection.
⚠️ Gotchas
- Still experimental — syntax differs across versions.
- Requires
"experimentalDecorators": true
intsconfig.json
.
🎯 One-Liner
“Decorators add metadata to classes/members — useful in frameworks like NestJS, but still experimental in TS.”
2. 📜 Type Annotations in JavaScript (TC39 Proposal)
Definition:
JavaScript itself may gain type syntax (stripped at runtime).
✅ Example (future JS)
function add(a: number, b: number): number {
return a + b;
}
- Types would be ignored at runtime, like TS today.
- TS would align with native JS type syntax.
🎯 One-Liner
“JS is moving toward built-in type annotations. TS will align, making gradual adoption easier.”
3. ⚖️ TypeScript vs Flow vs Others
✅ TypeScript
- Mainstream, broad ecosystem.
- Stronger tooling, VSCode integration.
✅ Flow (Meta)
- Better type inference in theory.
- Lost adoption due to ecosystem fragmentation.
✅ Elm / ReasonML / PureScript
- Stronger type systems, but niche.
🎯 One-Liner
“TS won the ecosystem war — Flow and others have niche uses, but TS dominates frontend and Node.”
4. 🆕 New & Recent TS Features
✅ satisfies
Operator (TS 4.9)
const theme = {
primary: "blue",
secondary: "red"
} satisfies Record<string, string>;
- Ensures structure without widening values.
✅ const
Type Parameters (coming soon)
function tuple<const T extends string[]>(...args: T): T {
return args;
}
const t = tuple("a", "b"); // type ["a", "b"]
✅ Variance Annotations (future)
- Explicitly mark generics as
in
(contravariant) orout
(covariant).
🎯 One-Liner
“Features like
satisfies
andconst
generics improve precision without hacks — future TS is about better inference + clarity.”
5. 🏗️ Migration Strategies
✅ JS → TS
- Enable
allowJs
+checkJs
. - Rename
.js
→.ts
gradually. - Add strict config (
noImplicitAny
,strictNullChecks
). - Replace JSDoc with real types.
✅ Flow → TS
- Use codemods (
flow-to-ts
). - Incrementally replace types.
✅ Legacy TS → Modern
- Remove
namespace
in favor of ES modules. - Switch to
strict
mode. - Replace
/// <reference>
with proper imports.
🎯 One-Liner
“Migrate incrementally: JS → TS with
checkJs
, Flow → TS with codemods, legacy TS → strict modules.”
6. 🏢 TypeScript at Scale
✅ Observations
- At very large scale (10M+ LOC), TS type-checking can bottleneck.
- Some companies (Google, Meta) experiment with faster type-checkers (SWC, Rome, incremental builds).
- Types become API contracts between teams — not just safety.
🎯 One-Liner
“At scale, TypeScript types are contracts between teams. Performance requires project references + incremental builds.”
7. 🔮 Future of TypeScript
✅ Trends
- Closer alignment with JavaScript (native type annotations).
- Better inference (const generics, variance).
- Compiler performance improvements (Rust-based checkers like
tsc-swc
). - More runtime type-checking integration (Zod + TS).
🎯 One-Liner
“The future of TS is tighter JS integration, better inference, and faster compilers — runtime validation will bridge static gaps.”
✅ Summary (Full Handbook)
This TypeScript handbook covers staff/architect-level depth:
- Fundamentals (type system vs runtime, primitives, assertions, narrowing, null checks, structural typing, interfaces vs types, enums vs literal types)
- Advanced Types (unions, intersections, literals, const assertions, template literals, conditional, mapped, utility types, recursive types)
- Generics (functions, constraints, defaults, keyof/indexed access, conditional generics, variance, HKTs, React generics)
- Type Inference & Compatibility (inference, widening/narrowing, assignability, any vs unknown vs never, enums, tuples vs arrays)
- JavaScript Interop (ambient declarations, @types, module augmentation, declaration merging, as const, tsconfig strictness, runtime validation, gradual migration)
- Type Guards & Narrowing (typeof, instanceof, in-operator, discriminated unions, custom predicates, assertion functions, exhaustiveness checks)
- Advanced Patterns (branded types, opaque types, recursive utilities, mapped utilities, variadic tuples, builder pattern, exact types, phantom types)
- TypeScript in React (props, children, hooks, context, refs, HOCs, generic components, event handlers, polymorphic components)
- Performance & Scaling (diagnostics, avoiding complex types, Babel/SWC vs tsc, incremental compilation, project references, import type, managing any, best practices)
- Ecosystem & Future (decorators, JS type annotations proposal, TS vs Flow, new features like satisfies/const generics, migration strategies, TS at scale, future trends)
This content originally appeared on DEV Community and was authored by Animesh Pandey

Animesh Pandey | Sciencx (2025-08-31T19:19:44+00:00) TypeScript Expert Revision Handbook. Retrieved from https://www.scien.cx/2025/08/31/typescript-expert-revision-handbook/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.