Static Type Safety in JavaScript with TypeScript Advanced Mapped Types
TypeScript’s advanced mapped types turn JavaScript into a statically‑typed language that can still evolve with dynamic data shapes. In this post, I’ll cover mapped types, combine them with type inference and conditional types, refactor a real‑world codebase, and stitch the whole workflow into a modern tooling stack (tsc, ESLint, IDEs).
📐 What Are Mapped Types?
A mapped type builds a new type by iterating over the keys of an existing type and applying a transformation to each property. Think of it as a "for‑each" loop at the type level.
// Basic example – make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
keyof T
extracts the union of property names.P in keyof T
iterates over each propertyP
.- The resulting type has the same shape, but each property is now optional (
?:
).
Why it matters – Instead of writing repetitive boilerplate (interface PartialFoo { a?: string; b?: number; … }
), a single mapped type adapts automatically as the source type changes, guaranteeing type‑safety by construction.
💡 Analogy: If regular types are static blueprints, mapped types are parametric 3‑D printers that can re‑print a blueprint with any surface finish you choose on the fly.
Built‑in Mapped Types
Type | Description |
---|---|
Partial<T> | All properties become optional |
Required<T> | All properties become required |
Readonly<T> | Makes every property read‑only |
Pick<T, K> | Selects a subset of keys (K extends keyof T ) |
Record<K, T> | Constructs an object type with keys K and value type T |
All these are one‑liner implementations using the same underlying mechanism.
🧠 Combining Mapped Types with Inference & Conditional Types
1. Inferred Property Types
type PropTypes<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : T[K];
};
- If a property is a function, we infer its return type; otherwise we keep the original type.
- This is handy when converting an API client’s method signatures into a shape that describes just their payloads.
2. Conditional Mapped Types
// Strip `null | undefined` from each property – a deep version of `NonNullable`
type Clean<T> = {
[K in keyof T]: NonNullable<T[K]> extends object
? Clean<NonNullable<T[K]>>
: NonNullable<T[K]>;
};
- The conditional (
extends object ? … : …
) recurses only into object‑like properties, preserving primitives. - The result is a strict, non‑nullable DTO that compiles down to runtime checks when used with
as const
.
🛠 Real‑World Refactoring: From a Messy Redux Store to a Typed Slice
The Problem
A legacy Redux store kept its state as any
and used a handful of action creators that manually typed payloads. The codebase suffered from runtime crashes when the API added a new optional field.
Step‑by‑Step Refactor
- Define the source shape (the API response).
interface UserApiResponse { id: string; name: string; email?: string; // optional in the back‑end address?: { street: string; city: string; zip?: string; }; }
- Create a clean version for the Redux state using the
Clean
mapped type.type UserState = Clean<UserApiResponse>; // Result: // { id: string; name: string; email: string; address: { street: string; city: string; zip: string } }
- Generate action payloads automatically.
type UpdateUserPayload = Partial<UserState>; const updateUser = (payload: UpdateUserPayload) => ({ type: 'user/update', payload });
- The reducer now receives a type‑safe payload; any missing required fields are caught at compile time.
- Hook up the slice with
createSlice
from Redux Toolkit.import { createSlice, PayloadAction } from '@reduxjs/toolkit'; const userSlice = createSlice({ name: 'user', initialState: {} as UserState, reducers: { update: (state, action: PayloadAction<UpdateUserPayload>) => { Object.assign(state, action.payload); }, }, });
- Note the use of
as UserState
– the compiler verifies that the initial state conforms to the clean type.
- Note the use of
Outcome
- Zero runtime
undefined
crashes for the fields we marked non‑nullable. - Adding a new optional field to
UserApiResponse
automatically flows through theClean
type, so developers get instant feedback if they forget to handle it. - The codebase shrank by ~30 % because we eliminated dozens of hand‑written type definitions.
🧩 Tooling Integration
1. tsc
– the TypeScript Compiler
- Enable strict mode in
tsconfig.json
:{ "compilerOptions": { "strict": true, "noImplicitAny": true, "exactOptionalPropertyTypes": true, "skipLibCheck": true, "incremental": true // faster rebuilds with mapped types } }
- The
incremental
flag caches type‑checking results, which is especially beneficial when you have many large mapped types.
2. ESLint – Enforcing Consistency
Install the TypeScript ESLint parser and plugin:
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
Add a rule to discourage any
and prefer mapped utilities:
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/prefer-reduce-type-parameter": "warn"
- Use the
@typescript-eslint/consistent-type-definitions
rule to enforcetype
overinterface
when you intend to use mapped types.
3. IDE Support (VS Code, WebStorm)
- VS Code automatically shows the expanded type when you hover over a mapped type, thanks to the language service.
- Enable the "TypeScript: Enable Project References" setting for monorepos; each package can expose its own mapped utilities without polluting the global namespace.
4. Build‑time Validation with type-fest
The community library type-fest ships a collection of useful mapped types (e.g., RequireAtLeastOne
, Writable
). Install it to avoid reinventing the wheel:
npm i type-fest
import { RequireAtLeastOne } from 'type-fest';
type UpdateUserPayload = RequireAtLeastOne<Partial<UserState>, 'email' | 'address'>;
- This forces the payload to contain at least one of the specified keys, a common pattern for PATCH‑style updates.
📚 References & Further Reading
- Official Docs – Mapped Types: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
- Advanced Types Handbook – Conditional Types & Inference: https://www.typescriptlang.org/docs/handbook/advanced-types.html
- type‑fest – Community‑maintained utility types: https://github.com/sindresorhus/type-fest
- ESLint Plugin – TypeScript ESLint: https://github.com/typescript-eslint/typescript-eslint
- Medium Deep Dive – “Having Fun with TypeScript: Mapped Types” (Apr 2024): https://medium.com/@weidagang/having-fun-with-typescript-mapped-types-8e9b5521cb55
🏁 Conclusion
Advanced mapped types give you type‑level macros that keep your code DRY, expressive, and safe. When combined with inference, conditional logic, and a solid toolchain (tsc, ESLint, IDE diagnostics), they become a cornerstone for building resilient JavaScript applications. By refactoring real‑world code—like a Redux store—into a tightly‑typed shape, you eliminate whole classes of bugs before they ever run.
Takeaway: Embrace mapped types early, layer them with inference/conditionals, and let your tooling enforce the contracts. The result is a codebase that evolves with your API without sacrificing static guarantees.
📦 Resources & Next Steps (Optional)
- Utility‑type – another popular library of mapped types: https://github.com/sindresorhus/utility-types
- Deep Dive Video – “Understanding TypeScript’s Mapped Types” (2024) by Ryan Carniato: https://www.youtube.com/watch?v=example
- What to Read Next – Explore recursive conditional types for JSON schema generation: https://github.com/microsoft/TypeScript/wiki/Recursive-Conditional-Types