Skip to content

Fix: TypeScript Mapped Type Errors — Type is Not Assignable to Mapped Type

FixDevs ·

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 property

Or 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 compatibility

Or 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 violationsReadonly<T> marks all properties as readonly. Assigning to them triggers a compile error, even if the runtime value is mutable.
  • Partial<T> makes properties optional — a Partial<User> doesn’t satisfy User because required properties may be missing.
  • Record<K, V> requires all keys — if K is a union type, every member of the union must appear as a key in the object literal.
  • Conditional types distributing over unionsT extends string ? A : B distributes across union members, which can produce unexpected union results.
  • Mapped type modifiers+readonly, -readonly, +?, -? add or remove modifiers. Misusing them causes type mismatches.
  • keyof with index signatureskeyof on a type with an index signature produces string | 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 removed

Fix 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 false

Prevent 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 string

Use 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 | true

Fix 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' allowed

Fix 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>;  // string

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 approach

keyof 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 typesas 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles