CtrlK
BlogDocsLog inGet started
Tessl Logo

mcollina/typescript-magician

Designs complex generic types, refactors `any` types to strict alternatives, creates type guards and utility types, and resolves TypeScript compiler errors. Use when the user asks about TypeScript (TS) types, generics, type inference, type guards, removing `any` types, strict typing, type errors, `infer`, `extends`, conditional types, mapped types, template literal types, branded/opaque types, or utility types like `Partial`, `Record`, `ReturnType`, and `Awaited`.

97

Quality

97%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

mapped-types.mdrules/

name:
mapped-types
description:
Creating new types by transforming existing type properties
metadata:
{"tags":"mapped-types, key-remapping, property-modifiers, index-signatures"}

Mapped Types

Overview

Mapped types allow you to create new types by transforming each property of an existing type. They iterate over keys and apply transformations to create new type structures.

Basic Syntax

type MappedType<T> = {
  [K in keyof T]: TransformedType;
};

Simple Examples

Make All Properties Optional

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

interface User {
  id: string;
  name: string;
  email: string;
}

type PartialUser = MyPartial<User>;
// { id?: string; name?: string; email?: string }

Make All Properties Required

type MyRequired<T> = {
  [K in keyof T]-?: T[K]; // -? removes optional modifier
};

Make All Properties Readonly

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

Make All Properties Mutable

type Mutable<T> = {
  -readonly [K in keyof T]: T[K]; // -readonly removes readonly modifier
};

Preserving Original Keys

When you just iterate over keyof T, you preserve the original keys:

type Preserve<T> = {
  [K in keyof T]: T[K]; // Same type, just recreated
};

Key Remapping with as

Transform keys while mapping using the as clause:

type RemapKeys<T> = {
  [K in keyof T as NewKeyType]: T[K];
};

Add Prefix to Keys

type Prefixed<T, P extends string> = {
  [K in keyof T as K extends string ? `${P}${K}` : K]: T[K];
};

interface User {
  name: string;
  age: number;
}

type PrefixedUser = Prefixed<User, "user_">;
// { user_name: string; user_age: number }

Remove Keys by Remapping to never

type RemoveFields<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

type UserWithoutEmail = RemoveFields<User, "email">;
// { id: string; name: string }

Transform Keys

type RemoveMapsPrefixFromObj<T> = {
  [K in keyof T as RemoveMaps<K>]: T[K];
};

type RemoveMaps<T> = T extends `maps:${infer Rest}` ? Rest : T;

interface ApiData {
  "maps:longitude": string;
  "maps:latitude": string;
}

type CleanData = RemoveMapsPrefixFromObj<ApiData>;
// { longitude: string; latitude: string }

Filtering Keys

Use conditional types in the as clause to filter:

// Only keep string properties
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }

Keep Only Required Properties

type RequiredKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];

type OnlyRequired<T> = Pick<T, RequiredKeys<T>>;

Keep Only Optional Properties

type OptionalKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? K : never;
}[keyof T];

type OnlyOptional<T> = Pick<T, OptionalKeys<T>>;

Transforming Property Types

Wrap All Properties in Promise

type Promisify<T> = {
  [K in keyof T]: Promise<T[K]>;
};

interface SyncApi {
  getUser(): User;
  getPost(): Post;
}

type AsyncApi = Promisify<SyncApi>;
// { getUser: Promise<() => User>; getPost: Promise<() => Post> }

Make All Properties Arrays

type Arrayify<T> = {
  [K in keyof T]: T[K][];
};

interface Single {
  name: string;
  count: number;
}

type Multiple = Arrayify<Single>;
// { name: string[]; count: number[] }

Nullable Properties

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

Deep Mapped Types

Apply transformations recursively:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

interface Nested {
  user: {
    profile: {
      name: string;
    };
  };
}

type ReadonlyNested = DeepReadonly<Nested>;
// All levels are readonly

Deep Partial

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

Practical Examples

Getters and Setters

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }

Event Handlers

type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    newValue: T[K],
    oldValue: T[K]
  ) => void;
};

interface State {
  count: number;
  name: string;
}

type StateHandlers = EventHandlers<State>;
// {
//   onCountChange: (newValue: number, oldValue: number) => void;
//   onNameChange: (newValue: string, oldValue: string) => void;
// }

Form Validation Errors

type ValidationErrors<T> = {
  [K in keyof T]?: string[];
};

interface RegistrationForm {
  email: string;
  password: string;
  confirmPassword: string;
}

type RegistrationErrors = ValidationErrors<RegistrationForm>;
// { email?: string[]; password?: string[]; confirmPassword?: string[] }

Combining Mapped Types

Pick with Transformation

type PickAndTransform<T, K extends keyof T> = {
  [P in K]: T[P] extends Function ? T[P] : Readonly<T[P]>;
};

Merge Two Types

type Merge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof B
    ? B[K]
    : K extends keyof A
    ? A[K]
    : never;
};

Index Signatures in Mapped Types

// Create index signature from union
type FromUnion<K extends string, V> = {
  [P in K]: V;
};

type Dict = FromUnion<"a" | "b" | "c", number>;
// { a: number; b: number; c: number }

Common Pitfalls

Forgetting String Key Check

Template literals require string keys:

// Error: Type 'K' is not assignable to type 'string'
type Wrong<T> = {
  [K in keyof T as `prefix_${K}`]: T[K];
};

// Correct: Check K extends string
type Correct<T> = {
  [K in keyof T as K extends string ? `prefix_${K}` : never]: T[K];
};

Losing Modifiers

Remapping can lose optional/readonly modifiers:

// Original optional modifier lost
type Transform<T> = {
  [K in keyof T as `new_${string & K}`]: T[K];
};

// Preserve optional with conditional
type TransformPreserve<T> = {
  [K in keyof T as `new_${string & K}`]+?: T[K];
};

Infinite Recursion

Deep mapped types can cause issues:

// Potential infinite recursion with circular types
type DeepReadonly<T> = {
  readonly [K in keyof T]: DeepReadonly<T[K]>;
};

// Add base case for primitives
type DeepReadonlySafe<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonlySafe<T[K]> }
  : T;

When to Use Mapped Types

  • Type transformations: Change modifiers (optional, readonly)
  • Key renaming: Add prefixes, suffixes, or transform key names
  • Property filtering: Remove or keep certain properties
  • Bulk operations: Apply same transformation to all properties
  • Type utilities: Build reusable type transformers

rules

array-index-access.md

as-const-typeof.md

builder-pattern.md

conditional-types.md

deep-inference.md

error-diagnosis.md

function-overloads.md

generics-basics.md

infer-keyword.md

mapped-types.md

opaque-types.md

template-literal-types.md

type-narrowing.md

utility-types.md

SKILL.md

tile.json