Fix: TypeScript Mapped Type Errors — Type is Not Assignable to Mapped Type
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
};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.
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>; // stringStill 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.
For related TypeScript issues, see Fix: TypeScript Declaration File Error and Fix: Python mypy Type 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.