Advanced Type Manipulation

1. Mapped Types with Key Remapping

Pattern Syntax Description Use Case
Basic Mapped Type {[K in Keys]: Type} Transform each property in a type - iterate over union of keys Make all properties readonly/optional
Key Remapping {[K in Keys as NewK]: Type} Transform property keys during mapping using as clause Prefix/suffix keys, filter properties
Template Remapping as `prefix${K}` Use template literal types to transform key names Add getters/setters prefixes
Conditional Remapping as K extends Cond ? T : F Conditionally include/exclude or transform keys Filter out specific properties
never for Filtering as K extends Bad ? never : K Exclude properties by mapping key to never Remove private/internal properties
Mapped Modifiers +readonly, -? Add (+) or remove (-) readonly/optional modifiers Create mutable/required versions

Example: Basic mapped types and modifiers

// Make all properties optional
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// Make all properties required (remove optional)
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// Make all properties readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

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

Example: Key remapping with template literals

// Add getter prefix to all properties
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

// Result: { getName: () => string; getAge: () => number; }
type UserGetters = Getters<User>;

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

// Combine getters and setters
type Accessors<T> = Getters<T> & Setters<T>;

Example: Filtering properties with conditional remapping

// Remove properties starting with underscore
type RemovePrivate<T> = {
    [K in keyof T as K extends `_${string}` ? never : K]: T[K];
};

interface Data {
    name: string;
    _id: number;
    _internal: boolean;
    value: number;
}

// Result: { name: string; value: number; }
type PublicData = RemovePrivate<Data>;

// Extract only function properties
type FunctionProps<T> = {
    [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

// Extract only non-function properties
type DataProps<T> = {
    [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

2. Template Literal Types and String Manipulation

Type Syntax Description Example
Template Literal `${A}${B}` Combine string literal types - creates union of all combinations `user_${number}`
Uppercase<S> Uppercase<'hello'> Convert string literal to uppercase 'HELLO'
Lowercase<S> Lowercase<'HELLO'> Convert string literal to lowercase 'hello'
Capitalize<S> Capitalize<'hello'> Capitalize first character of string 'Hello'
Uncapitalize<S> Uncapitalize<'Hello'> Lowercase first character of string 'hello'
Pattern Matching S extends `${infer A}${infer B}` Extract parts of string using infer in template Parse string patterns

Example: Creating event name types

// Generate event names from property keys
type EventNames<T> = {
    [K in keyof T as `${string & K}Changed`]: (value: T[K]) => void;
};

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

// Result: {
//   countChanged: (value: number) => void;
//   nameChanged: (value: string) => void;
//   activeChanged: (value: boolean) => void;
// }
type StateEvents = EventNames<State>;

// HTTP method + path combinations
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIPath = '/users' | '/posts' | '/comments';
type APIRoute = `${HTTPMethod} ${APIPath}`;
// Result: 'GET /users' | 'POST /users' | ... (12 combinations)

Example: String manipulation utilities

// Convert camelCase to kebab-case
type KebabCase<S extends string> = 
    S extends `${infer First}${infer Rest}`
        ? First extends Uppercase<First>
            ? `-${Lowercase<First>}${KebabCase<Rest>}`
            : `${First}${KebabCase<Rest>}`
        : S;

type Result1 = KebabCase<'userName'>; // 'user-name'
type Result2 = KebabCase<'getAPIData'>; // 'get-a-p-i-data'

// Extract email domain
type ExtractDomain<T extends string> = 
    T extends `${string}@${infer Domain}` ? Domain : never;

type Domain = ExtractDomain<'user@example.com'>; // 'example.com'

// Split string by delimiter
type Split<S extends string, D extends string> = 
    S extends `${infer T}${D}${infer U}` 
        ? [T, ...Split<U, D>] 
        : [S];

type Parts = Split<'a.b.c', '.'>; // ['a', 'b', 'c']

3. Recursive Types and Recursive Conditional Types

Pattern Description Use Case Limitation
Recursive Type Alias Type that references itself in definition Tree structures, nested objects Max depth ~50 levels
Recursive Conditional Conditional type that recurses on type structure Deep traversal, transformation Compiler depth limit
Tail Recursion Recursion pattern with accumulator parameter Better performance, deeper nesting Still limited by compiler
Structural Recursion Recurse through object/array structure Deep readonly, deep partial Complex nested types

Example: Basic recursive types

// Tree structure
interface TreeNode<T> {
    value: T;
    left?: TreeNode<T>;
    right?: TreeNode<T>;
}

// JSON value representation
type JSONValue = 
    | string 
    | number 
    | boolean 
    | null 
    | JSONValue[] 
    | { [key: string]: JSONValue };

// Linked list
type LinkedList<T> = {
    value: T;
    next: LinkedList<T> | null;
};

// Nested array flattening type
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Deep = Flatten<number[][][]>; // number

Example: Deep readonly and partial

// Deep readonly - make all nested properties readonly
type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object
        ? DeepReadonly<T[P]>
        : T[P];
};

interface NestedData {
    user: {
        name: string;
        address: {
            street: string;
            city: string;
        };
    };
}

type ReadonlyData = DeepReadonly<NestedData>;
// All nested properties are readonly

// Deep partial - make all nested properties optional
type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object
        ? DeepPartial<T[P]>
        : T[P];
};

// Deep required - make all nested properties required
type DeepRequired<T> = {
    [P in keyof T]-?: T[P] extends object
        ? DeepRequired<T[P]>
        : T[P];
};

Example: Path string type for nested objects

// Generate all valid property paths
type Paths<T, Prefix extends string = ''> = {
    [K in keyof T]: K extends string
        ? T[K] extends object
            ? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
            : `${Prefix}${K}`
        : never;
}[keyof T];

interface Data {
    user: {
        name: string;
        address: {
            city: string;
        };
    };
    count: number;
}

// Result: 'user' | 'user.name' | 'user.address' | 'user.address.city' | 'count'
type DataPaths = Paths<Data>;

// Get value type at path
type PathValue<T, P extends string> = 
    P extends `${infer K}.${infer Rest}`
        ? K extends keyof T
            ? PathValue<T[K], Rest>
            : never
        : P extends keyof T
            ? T[P]
            : never;

type CityType = PathValue<Data, 'user.address.city'>; // string

4. Higher-Order Types and Type-level Programming

Concept Description Pattern Use Case
Type Constructor Generic type that takes types as parameters type Box<T> = { value: T } Container types, wrappers
Higher-Order Type Type that takes/returns type constructors type Apply<F, T> Generic transformations
Type-level Function Conditional type that computes new type type If<C, T, F> Conditional logic
Type Composition Combine multiple type transformations Compose<F, G, T> Pipeline transformations
Type-level Iteration Build types through recursion/iteration Tuple building, counting Compile-time computation

Example: Type-level utilities

// Identity function at type level
type Identity<T> = T;

// Constant type - always returns same type
type Const<A, B> = A;

// Type-level if/else
type If<Cond extends boolean, Then, Else> = 
    Cond extends true ? Then : Else;

// Type equality check
type Equals<X, Y> = 
    (<T>() => T extends X ? 1 : 2) extends 
    (<T>() => T extends Y ? 1 : 2) ? true : false;

type Test1 = Equals<string, string>; // true
type Test2 = Equals<string, number>; // false

// Type-level AND
type And<A extends boolean, B extends boolean> = 
    A extends true ? (B extends true ? true : false) : false;

// Type-level OR
type Or<A extends boolean, B extends boolean> = 
    A extends true ? true : (B extends true ? true : false);

Example: Tuple and array manipulation

// Tuple length
type Length<T extends any[]> = T['length'];

// Prepend element to tuple
type Prepend<E, T extends any[]> = [E, ...T];

// Append element to tuple
type Append<T extends any[], E> = [...T, E];

// Concat tuples
type Concat<T extends any[], U extends any[]> = [...T, ...U];

// Reverse tuple
type Reverse<T extends any[]> = 
    T extends [infer First, ...infer Rest]
        ? [...Reverse<Rest>, First]
        : [];

type Original = [1, 2, 3];
type Reversed = Reverse<Original>; // [3, 2, 1]

// Build tuple of length N
type Tuple<N extends number, T = unknown, Acc extends T[] = []> = 
    Acc['length'] extends N 
        ? Acc 
        : Tuple<N, T, [T, ...Acc]>;

type FiveItems = Tuple<5, string>; // [string, string, string, string, string]

Example: Type-level arithmetic (simplified)

// Add 1 to tuple length (increment)
type Inc<T extends any[]> = [...T, any];

// Subtract 1 from tuple length (decrement)
type Dec<T extends any[]> = 
    T extends [any, ...infer Rest] ? Rest : [];

// Add two numbers using tuples
type Add<A extends number, B extends number> = 
    [...Tuple<A>, ...Tuple<B>]['length'];

type Result = Add<3, 5>; // 8 (limited by recursion depth)

// Range type - generate union of numbers
type Range<
    From extends number,
    To extends number,
    Acc extends number[] = [],
    Result = never
> = Acc['length'] extends To
    ? Result | To
    : Range<From, To, Inc<Acc>, 
        Acc['length'] extends From 
            ? Result | Acc['length'] 
            : Result>;

type ZeroToFive = Range<0, 5>; // 0 | 1 | 2 | 3 | 4 | 5

5. Brand Types and Nominal Typing Patterns

Pattern Technique Purpose Advantage
Brand Property Add unique symbol property Distinguish structurally identical types Type safety for primitives
Phantom Type Generic parameter not used in structure Tag type with metadata No runtime overhead
Opaque Type Hide implementation details Prevent direct construction Encapsulation
Newtype Pattern Wrapper type around primitive Domain-specific types Self-documenting code

Example: Basic branding with symbols

// Brand using unique symbol
declare const UserIdBrand: unique symbol;
type UserId = string & { [UserIdBrand]: true };

declare const ProductIdBrand: unique symbol;
type ProductId = string & { [ProductIdBrand]: true };

// Constructor functions ensure type safety
function createUserId(id: string): UserId {
    // Validation logic here
    return id as UserId;
}

function createProductId(id: string): ProductId {
    return id as ProductId;
}

// Type-safe usage
function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }

const userId = createUserId('user-123');
const productId = createProductId('prod-456');

getUser(userId); // ✓ OK
getUser(productId); // ✗ Error: ProductId not assignable to UserId
getUser('user-789'); // ✗ Error: string not assignable to UserId

Example: Branded primitive types

// Email type
type Email = string & { readonly __brand: 'Email' };

function isValidEmail(str: string): str is Email {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}

function sendEmail(to: Email, subject: string) {
    console.log(`Sending to ${to}: ${subject}`);
}

const input = 'user@example.com';
if (isValidEmail(input)) {
    sendEmail(input, 'Hello'); // ✓ Type guard narrows to Email
}
sendEmail(input, 'Hello'); // ✗ Error: string not Email

// Positive integer type
type PositiveInt = number & { readonly __brand: 'PositiveInt' };

function toPositiveInt(n: number): PositiveInt {
    if (!Number.isInteger(n) || n <= 0) {
        throw new Error('Not a positive integer');
    }
    return n as PositiveInt;
}

function setArraySize(size: PositiveInt) { /* ... */ }

Example: Phantom types for state machines

// State machine with phantom types
type State = 'Initial' | 'Loaded' | 'Processed';

interface DataContainer<S extends State> {
    data: unknown;
    _state?: S; // Phantom - not used at runtime
}

// State-specific operations
function load(): DataContainer<'Loaded'> {
    return { data: 'loaded data' };
}

function process(
    container: DataContainer<'Loaded'>
): DataContainer<'Processed'> {
    return { data: 'processed' };
}

function save(container: DataContainer<'Processed'>) {
    console.log('Saving:', container.data);
}

// Type-safe workflow
const initial: DataContainer<'Initial'> = { data: null };
const loaded = load();
const processed = process(loaded);
save(processed); // ✓ OK

save(loaded); // ✗ Error: 'Loaded' not assignable to 'Processed'
process(processed); // ✗ Error: 'Processed' not assignable to 'Loaded'
Note: Brand types enable nominal typing in TypeScript's structural type system. Use them for domain primitives (IDs, emails, URLs), validated data, and state machine transitions. The runtime cost is zero since brands are compile-time only.

6. Type-only Imports and Exports

Syntax Purpose Behavior Benefit
import type Import only for type checking Erased during compilation - no runtime code Avoid circular dependencies, smaller bundles
export type Export only type information Type-only export, removed at runtime Clear intent, separate concerns
inline type import import { type T } Mix type and value imports Flexibility, granular control
importsNotUsedAsValues Compiler option for side effects Control import preservation Prevent unwanted side effects

Example: Type-only imports

// types.ts
export interface User {
    id: string;
    name: string;
}

export type UserId = string;

export const DEFAULT_USER: User = {
    id: '0',
    name: 'Guest'
};

// user.ts - type-only import
import type { User, UserId } from './types';
// DEFAULT_USER not imported - no runtime dependency

function processUser(user: User): UserId {
    return user.id;
}

// ✗ Error: Cannot use type import as value
// const guest = DEFAULT_USER;

Example: Inline type imports (TS 4.5+)

// Mixed imports - types and values
import { createUser, type User, type UserId } from './types';
//       ^^^^^^^^^^  ^^^^      ^^^^^^^^^^^^
//       value       type      type

// Can use createUser at runtime
const newUser = createUser();

// Can use User and UserId for typing
function validate(user: User): UserId {
    return user.id;
}

// Re-export with inline type
export { createUser, type User } from './types';

Example: Breaking circular dependencies

// module-a.ts
import type { TypeB } from './module-b';
// Only imports type, not runtime code

export interface TypeA {
    b: TypeB;
    name: string;
}

export function createA(): TypeA { /* ... */ }

// module-b.ts
import type { TypeA } from './module-a';
// Circular reference OK with type-only import

export interface TypeB {
    a: TypeA;
    value: number;
}

export function createB(): TypeB { /* ... */ }

// Without type-only imports, this would cause circular dependency error

Example: Type-only exports

// api-types.ts - pure type definitions
export type { User, Product, Order } from './models';
// Only export types, not any runtime code

// Re-export types from multiple sources
export type {
    RequestHandler,
    Middleware
} from './handlers';

export type {
    Config,
    Environment
} from './config';

// Can be imported in consuming code
import type { User, Product, RequestHandler } from './api-types';
Note: Use import type when you only need types for annotations. This is especially important for:
  • Breaking circular dependencies between modules
  • Importing from libraries where you don't want side effects
  • Reducing bundle size by avoiding unused imports
  • Making dependencies explicit in type-only contexts
Enable "verbatimModuleSyntax": true in tsconfig for stricter type import checking.
Warning: Type-only imports are erased at runtime. Don't use import type for:
  • Classes you need to instantiate
  • Functions you need to call
  • Values you need to reference
  • Modules with required side effects (initialization code)

Advanced Type Manipulation Summary

  • Mapped types transform object properties with key remapping for prefixes, filters, and transformations
  • Template literal types enable string manipulation and pattern matching at the type level
  • Recursive types handle nested structures like trees, JSON, and deep transformations (limited by compiler depth)
  • Higher-order types provide type-level programming with functions, composition, and computation
  • Brand types add nominal typing to distinguish structurally identical types for domain modeling
  • Type-only imports reduce bundle size, break circular deps, and clarify type-only dependencies