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