Fix: TypeScript Mapped Type Errors — Type is Not Assignable to Mapped Type
Part of: React & Frontend Errors
Quick Answer
How to fix TypeScript mapped type errors — Partial, Required, Readonly, Record, Pick, Omit, conditional types, template literal types, and distributive behavior.
The Problem
TypeScript rejects an assignment to a mapped type:
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { name: 'Alice', age: 30 };
user.name = 'Bob'; // Error: Cannot assign to 'name' because it is a read-only propertyOr a Partial<T> type causes unexpected errors downstream:
function update(user: Partial<User>): User {
return { ...defaultUser, ...user }; // Error: Type 'Partial<User>' is not assignable to type 'User'
}Or a custom mapped type produces incorrect types:
type Nullable<T> = { [K in keyof T]: T[K] | null };
type NullableUser = Nullable<User>;
// Expected: { name: string | null; age: number | null }
// Actual: errors about index signature compatibilityOr Record<K, V> doesn’t accept the expected key type:
type Status = 'active' | 'inactive';
const statusMap: Record<Status, string> = {
active: 'Active',
// Error: Property 'inactive' is missing
};In production, mapped type errors surface as CI type-check failures that block every deployment until the type is fixed or an assertion is added. The blast radius is “no deploys for anyone on the branch,” and the pressure to slap on as any grows with every minute the pipeline is red.
Why This Happens
TypeScript’s mapped types transform the shape of existing types. Errors arise from several common patterns:
- Mutability violations —
Readonly<T>marks all properties asreadonly. Assigning to them triggers a compile error, even if the runtime value is mutable. Partial<T>makes properties optional — aPartial<User>doesn’t satisfyUserbecause required properties may be missing.Record<K, V>requires all keys — ifKis a union type, every member of the union must appear as a key in the object literal.- Conditional types distributing over unions —
T extends string ? A : Bdistributes across union members, which can produce unexpected union results. - Mapped type modifiers —
+readonly,-readonly,+?,-?add or remove modifiers. Misusing them causes type mismatches. keyofwith index signatures —keyofon a type with an index signature producesstring | number, not a specific literal union.
These errors often appear silently in local development (especially if the IDE swallows diagnostics) and only become blocking when they hit the CI tsc --noEmit step. The gap between “it compiles on my machine” and “it fails in the pipeline” is usually caused by different tsconfig strictness settings, different TypeScript versions between local and CI, or skipLibCheck hiding transitive type mismatches locally.
Understanding the root cause matters because the wrong fix — casting to any, disabling strictNullChecks, or suppressing with @ts-ignore — silences the error but introduces runtime bugs that surface weeks later in production.
Fix 1: Understand and Use Built-in Mapped Types
TypeScript’s utility types solve common patterns:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// Partial<T> — makes all properties optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; role?: 'admin' | 'user' }
// Required<T> — makes all properties required (opposite of Partial)
type RequiredUser = Required<PartialUser>;
// { id: number; name: string; email: string; role: 'admin' | 'user' }
// Readonly<T> — makes all properties read-only
type FrozenUser = Readonly<User>;
// { readonly id: number; readonly name: string; ... }
// Record<K, V> — creates an object type with keys K and values V
type UserMap = Record<string, User>; // { [key: string]: User }
type RolePermissions = Record<User['role'], string[]>; // { admin: string[]; user: string[] }
// Pick<T, K> — keeps only specified properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string }
// Omit<T, K> — removes specified properties
type PublicUser = Omit<User, 'email' | 'role'>;
// { id: number; name: string }
// NonNullable<T> — removes null and undefined from union
type ValidId = NonNullable<number | null | undefined>; // number
// ReturnType<T> — extracts return type of a function
type FetchResult = ReturnType<typeof fetch>; // Promise<Response>
// Parameters<T> — extracts parameter types as a tuple
type FetchParams = Parameters<typeof fetch>; // [input: RequestInfo | URL, init?: RequestInit]Fix 2: Fix PartialDownstream Errors
Partial<T> makes properties optional — functions expecting the full type reject Partial<T>:
interface User {
name: string;
email: string;
age: number;
}
// WRONG — Partial<User> doesn't satisfy User
function processUser(user: Partial<User>): User {
return user; // Error: 'name', 'email', 'age' may be undefined
}
// CORRECT — merge with defaults
const defaultUser: User = { name: 'Anonymous', email: '', age: 0 };
function processUser(updates: Partial<User>): User {
return { ...defaultUser, ...updates }; // Always a complete User
}
// CORRECT — use required fields approach
function createUser(required: Pick<User, 'name' | 'email'>, optional?: Partial<Omit<User, 'name' | 'email'>>): User {
return {
age: 0,
...optional,
...required, // Required fields always present
};
}
// CORRECT — validate at runtime and narrow the type
function ensureComplete(user: Partial<User>): User {
if (!user.name || !user.email || user.age === undefined) {
throw new Error('User is incomplete');
}
// TypeScript knows all fields are defined after the checks above
return user as User; // Safe cast after runtime validation
}Fix 3: Build Custom Mapped Types
Custom mapped types transform all properties in a type:
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;
// { name: string | null; email: string | null; age: number | null }
// Make all properties into async getters (functions returning Promise)
type Async<T> = {
[K in keyof T]: () => Promise<T[K]>;
};
type AsyncUser = Async<Pick<User, 'name' | 'email'>>;
// { name: () => Promise<string>; email: () => Promise<string> }
// Deep readonly — recursively makes all nested types readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type FrozenConfig = DeepReadonly<{
server: { host: string; port: number };
database: { url: string };
}>;
// All properties and nested properties are readonly
// Rename keys by adding a prefix
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
type PrefixedUser = Prefixed<Pick<User, 'name' | 'email'>, 'user'>;
// { userName: string; userEmail: string }Filter keys by value type using as clause:
// Keep only properties of a specific type
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Config {
host: string;
port: number;
name: string;
debug: boolean;
}
type StringConfig = StringProperties<Config>;
// { host: string; name: string } — port and debug removedFix 4: Fix Conditional Type Distribution
Conditional types distribute over union members, which can produce unexpected results:
type IsString<T> = T extends string ? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
type Result3 = IsString<string | number>; // boolean — distributes: (true) | (false) = boolean
// WRONG expectation: Result3 is boolean, not a single true or falsePrevent distribution by wrapping in a tuple:
// Non-distributive version
type IsStringExact<T> = [T] extends [string] ? true : false;
type Result = IsStringExact<string | number>; // false — (string | number) doesn't extend stringUse distribution intentionally:
// Extract only string members from a union
type StringMembers<T> = T extends string ? T : never;
type Mixed = 'active' | 'inactive' | 42 | true;
type OnlyStrings = StringMembers<Mixed>; // 'active' | 'inactive'
// Exclude types from a union (same as built-in Exclude<T, U>)
type Exclude<T, U> = T extends U ? never : T;
type WithoutString = Exclude<Mixed, string>; // 42 | trueFix 5: Template Literal Types
TypeScript 4.1+ supports template literal types for string manipulation:
type EventName = 'click' | 'focus' | 'blur';
// Generate 'onClick' | 'onFocus' | 'onBlur'
type HandlerName = `on${Capitalize<EventName>}`;
// Generate object type with handler functions
type EventHandlers = {
[K in EventName as `on${Capitalize<K>}`]: (event: Event) => void;
};
// { onClick: (event: Event) => void; onFocus: ...; onBlur: ... }
// Extract parts from string literals
type ExtractRoute<S extends string> =
S extends `${infer Method} ${infer Path}` ? { method: Method; path: Path } : never;
type RouteInfo = ExtractRoute<'GET /users'>;
// { method: 'GET'; path: '/users' }
// Generate getter method names
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }Fix 6: Fix Record<K, V> Errors
Record<K, V> requires the object to have every key in K:
type Status = 'active' | 'inactive' | 'pending';
// WRONG — missing 'pending' key
const statusLabels: Record<Status, string> = {
active: 'Active',
inactive: 'Inactive',
// Error: Property 'pending' is missing in type
};
// CORRECT — all keys present
const statusLabels: Record<Status, string> = {
active: 'Active',
inactive: 'Inactive',
pending: 'Pending',
};
// When you want optional values — use Partial<Record<K, V>>
const partialLabels: Partial<Record<Status, string>> = {
active: 'Active', // Only some statuses need labels
};
// Dynamic keys with runtime validation
function createStatusMap<T>(statuses: Status[], getValue: (s: Status) => T): Record<Status, T> {
// TypeScript doesn't know all statuses are covered at compile time
return Object.fromEntries(
statuses.map(s => [s, getValue(s)])
) as Record<Status, T>;
}Record vs index signature:
// Index signature — any string key
type GenericMap = { [key: string]: string };
const m: GenericMap = { a: '1', b: '2' }; // Open — any key allowed
// Record — specific keys only (when K is a union)
type SpecificMap = Record<'a' | 'b', string>;
const s: SpecificMap = { a: '1', b: '2' }; // Closed — only 'a' and 'b' allowedFix 7: Infer Types Within Conditional Types
infer extracts types from within conditional type checks:
// Extract the element type from an array
type ElementType<T> = T extends (infer E)[] ? E : never;
type Nums = ElementType<number[]>; // number
type Strs = ElementType<string[]>; // string
type Nope = ElementType<string>; // never — not an array
// Extract the resolved type of a Promise
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T; // Recursive for nested
type Resolved = Awaited<Promise<Promise<string>>>; // string
// Extract function return type (same as built-in ReturnType)
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
// Extract constructor instance type
type InstanceOf<T> = T extends new (...args: any[]) => infer I ? I : never;
class User { name = ''; }
type UserInstance = InstanceOf<typeof User>; // User
// Extract first argument type
type FirstArg<T> = T extends (arg: infer A, ...rest: any[]) => any ? A : never;
function greet(name: string, greeting?: string) { return `${greeting} ${name}`; }
type NameType = FirstArg<typeof greet>; // stringFix 8: Avoid Unsafe Workarounds That Hide Bugs
When a mapped type error blocks the CI pipeline, the temptation is to reach for as any or @ts-ignore. These workarounds silence the compiler but introduce runtime risk:
// DANGEROUS — as any hides the real problem
function getUser(data: unknown): User {
return data as any; // No type checking at all
// Ships to production → crashes on missing fields
}
// DANGEROUS — @ts-ignore makes the next line invisible to tsc
// @ts-ignore
const value: Required<Config> = partialConfig;
// If partialConfig is missing keys, this crashes at runtime
// SAFER — use a type guard or assertion function
function assertUser(data: unknown): asserts data is User {
if (typeof data !== 'object' || data === null) {
throw new TypeError('Expected an object');
}
const obj = data as Record<string, unknown>;
if (typeof obj.name !== 'string') throw new TypeError('Missing name');
if (typeof obj.email !== 'string') throw new TypeError('Missing email');
if (typeof obj.age !== 'number') throw new TypeError('Missing age');
}
// SAFER — use satisfies for validation without widening
const config = {
host: 'localhost',
port: 3000,
debug: true,
} satisfies Record<string, string | number | boolean>;
// TypeScript checks the shape without losing literal typesPro Tip: If your CI pipeline is red and you must unblock deploys immediately, add a well-scoped type assertion on a single expression rather than as any on an entire object. Then open a follow-up ticket to fix the underlying type. A narrow assertion like (value as Pick<User, 'name'>).name is auditable. A blanket as any is not.
When CI Blocks Deploy: The Production Incident Angle
A mapped type error in CI looks like a low-severity issue — “just a type error” — but the impact compounds:
Blast radius. Every PR on the branch (or the entire repo, if CI runs tsc across the whole project) is blocked. No features, no hotfixes, no reverts ship until the type error is resolved. In a monorepo, a single broken mapped type in a shared package can block dozens of teams.
Detection. The error appears only in CI if local TypeScript settings differ. Common causes: a developer has skipLibCheck: true locally, the CI uses a newer TypeScript version, or the IDE uses a different tsconfig path. Pin TypeScript versions in package.json (not ^5.x but 5.4.5) and ensure local tsc --noEmit runs against the same config as CI.
Recovery timeline. If the fix is straightforward (add a missing Record key, remove a bad as const), recovery takes minutes. If the error involves a complex generic chain spanning multiple files, diagnosis can take hours. During that window, other developers may attempt their own workarounds, creating merge conflicts or introducing as any casts that persist long after the original error is fixed.
Prevention. Run tsc --noEmit in a pre-commit or pre-push hook so type errors surface before they reach CI:
// package.json — lint-staged + husky example
{
"lint-staged": {
"*.{ts,tsx}": ["tsc --noEmit --pretty"]
}
}For monorepos, consider incremental type checking with --build mode and project references so that a type error in one package doesn’t require re-checking the entire tree.
Still Not Working?
Recursive type limit — deeply recursive mapped types can hit TypeScript’s recursion limit. TypeScript shows Type instantiation is excessively deep and possibly infinite. Limit recursion depth or use interface extension instead:
// Instead of recursive mapped type for deep objects,
// consider using interface extension or a simpler approachkeyof with index signatures returns string | number — if your type has [key: string]: any, then keyof T is string | number, not a specific union. This can cause mapped type keys to be string | number instead of specific literals.
as const and mapped types — as const makes an object’s values literal types. Combining with keyof typeof obj gives you a union of literal key types:
const routes = { home: '/', about: '/about', contact: '/contact' } as const;
type Route = typeof routes[keyof typeof routes]; // '/' | '/about' | '/contact'Circular type references — a mapped type that references itself can cause Type alias circularly references itself errors. Use interface for self-referential types.
Mapped types losing optional modifiers after transformation — when you map over a type and apply a transformation in the value position, the optional modifier (?) is preserved by default. If you need to remove it, use -? explicitly: { [K in keyof T]-?: NonNullable<T[K]> }. This is what Required<T> does internally.
Homomorphic vs non-homomorphic mapped types — a mapped type of the form { [K in keyof T]: ... } is homomorphic: it preserves modifiers (readonly, ?) from the original type. A mapped type like { [K in 'a' | 'b']: ... } is non-homomorphic and does not carry over modifiers. If your custom mapped type is unexpectedly making properties required or mutable, check whether you’re iterating over keyof T or a standalone union.
For related TypeScript issues, see Fix: TypeScript Declaration File Error, Fix: TypeScript Generic Constraint Error, Fix: TypeScript Conditional Types Not Working, and Fix: TypeScript Discriminated Union Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.
Fix: date-fns Not Working — Wrong Timezone Output, Invalid Date, or Locale Not Applied
How to fix date-fns issues — timezone handling with date-fns-tz, parseISO vs new Date, locale import and configuration, DST edge cases, v3 ESM migration, and common format pattern mistakes.
Fix: Zod Validation Not Working — safeParse Returns Wrong Error, transform Breaks Type, or discriminatedUnion Fails
How to fix Zod schema validation issues — parse vs safeParse, transform and preprocess, refine for cross-field validation, discriminatedUnion, error formatting, and common schema mistakes.