Explain mapped types and template literal types. Provide a practical example where these would be beneficial.

10 minadvancedtypescriptmappedtypestemplateliteral

Quick Answer

Mapped types and template literal types are advanced TypeScript features that allow you to create new types by transforming existing ones. They're powerful tools for creating reusable and flexible type definitions.

Detailed Answer

Explain mapped types and template literal types. Provide a practical example where these would be beneficial.

Answer: Mapped types and template literal types are advanced TypeScript features that allow you to create new types by transforming existing ones. They're powerful tools for creating reusable and flexible type definitions.

Mapped Types:

Mapped types allow you to create new types by iterating over the keys of an existing type and transforming them.

Basic Mapped Type Syntax:

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

Common Mapped Type Patterns:

  1. Making All Properties Optional:
type Partial<T> = {
  [P in keyof T]?: T[P];
};

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

type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
  1. Making All Properties Required:
type Required<T> = {
  [P in keyof T]-?: T[P];
};

interface Config {
  apiUrl?: string;
  timeout?: number;
}

type RequiredConfig = Required<Config>;
// { apiUrl: string; timeout: number; }
  1. Making All Properties Readonly:
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }
  1. Transforming Property Types:
type Stringify<T> = {
  [K in keyof T]: string;
};

type StringifiedUser = Stringify<User>;
// { id: string; name: string; email: string; }
  1. Conditional Property Transformation:
type NonNullable<T> = {
  [P in keyof T]: T[P] extends null | undefined ? never : T[P];
};

type NonNullableUser = NonNullable<{
  id: number;
  name: string | null;
  email: string | undefined;
}>;
// { id: number; name: never; email: never; }

Advanced Mapped Type Patterns:

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

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }
  1. Filtering Properties:
type FunctionProperties<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

interface MixedObject {
  name: string;
  age: number;
  greet: () => void;
  calculate: (x: number) => number;
}

type FunctionsOnly = FunctionProperties<MixedObject>;
// { greet: () => void; calculate: (x: number) => number; }
  1. Conditional Key Mapping:
type ApiEndpoints<T> = {
  [K in keyof T as T[K] extends { id: any } ? `get${Capitalize<string & K>}` : never]: 
    (id: T[K]['id']) => Promise<T[K]>;
};

interface ApiResources {
  user: { id: number; name: string };
  product: { id: string; title: string };
  category: { name: string }; // No id property
}

type Endpoints = ApiEndpoints<ApiResources>;
// { getUser: (id: number) => Promise<{ id: number; name: string }>; 
//   getProduct: (id: string) => Promise<{ id: string; title: string }>; }

Template Literal Types:

Template literal types allow you to create string literal types by combining other types.

Basic Template Literal Syntax:

type TemplateLiteral = `prefix-${string}-suffix`;

Common Template Literal Patterns:

  1. String Concatenation:
type EventName = 'click' | 'hover' | 'focus';
type EventHandler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onHover' | 'onFocus'

type CSSProperty = 'margin' | 'padding' | 'border';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type CSSPropertyWithDirection = `${CSSProperty}-${CSSDirection}`;
// 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left' | 
// 'padding-top' | 'padding-right' | 'padding-bottom' | 'padding-left' | 
// 'border-top' | 'border-right' | 'border-bottom' | 'border-left'
  1. API Route Generation:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'users' | 'posts' | 'comments';
type ApiRoute = `${HttpMethod} /api/${Resource}`;
// 'GET /api/users' | 'GET /api/posts' | 'GET /api/comments' | 
// 'POST /api/users' | 'POST /api/posts' | 'POST /api/comments' | ...
  1. CSS Class Generation:
type Component = 'button' | 'input' | 'card';
type Variant = 'primary' | 'secondary' | 'danger';
type Size = 'sm' | 'md' | 'lg';
type CSSClass = `${Component}-${Variant}-${Size}`;
// 'button-primary-sm' | 'button-primary-md' | 'button-primary-lg' | ...

Advanced Template Literal Patterns:

  1. String Manipulation Utilities:
type Capitalize<S extends string> = S extends `${infer F}${infer R}` 
  ? `${Uppercase<F>}${R}` 
  : S;

type Uncapitalize<S extends string> = S extends `${infer F}${infer R}` 
  ? `${Lowercase<F>}${R}` 
  : S;

type CamelCase<S extends string> = S extends `${infer P1}-${infer P2}${infer P3}`
  ? `${P1}${Capitalize<`${P2}${P3}`>}`
  : S;

type PascalCase<S extends string> = Capitalize<CamelCase<S>>;

type KebabCase<S extends string> = S extends `${infer C}${infer T}`
  ? T extends Uncapitalize<T>
    ? `${Uncapitalize<C>}${KebabCase<T>}`
    : `${Uncapitalize<C>}-${KebabCase<Uncapitalize<T>>}`
  : S;
  1. Path Parameter Extraction:
type ExtractParams<T extends string> = T extends `${string}:${infer P}/${infer R}`
  ? P | ExtractParams<R>
  : T extends `${string}:${infer P}`
  ? P
  : never;

type RouteParams = ExtractParams<'/users/:id/posts/:postId'>;
// 'id' | 'postId'

Practical Example: Form Builder System

Here's a comprehensive example that combines mapped types and template literal types to create a type-safe form builder:

// Base field types
type FieldType = 'text' | 'email' | 'number' | 'select' | 'checkbox' | 'textarea';

// Field configuration
interface BaseField {
  type: FieldType;
  label: string;
  required?: boolean;
  placeholder?: string;
}

interface SelectField extends BaseField {
  type: 'select';
  options: Array<{ value: string; label: string }>;
}

interface CheckboxField extends BaseField {
  type: 'checkbox';
  defaultChecked?: boolean;
}

type Field = BaseField | SelectField | CheckboxField;

// Form schema definition
interface FormSchema {
  [fieldName: string]: Field;
}

// Generate form data type from schema
type FormData<T extends FormSchema> = {
  [K in keyof T]: T[K] extends { type: 'number' }
    ? number
    : T[K] extends { type: 'checkbox' }
    ? boolean
    : string;
};

// Generate validation rules type
type ValidationRules<T extends FormSchema> = {
  [K in keyof T as `${string & K}Validation`]: {
    required?: boolean;
    minLength?: T[K] extends { type: 'text' | 'textarea' } ? number : never;
    maxLength?: T[K] extends { type: 'text' | 'textarea' } ? number : never;
    min?: T[K] extends { type: 'number' } ? number : never;
    max?: T[K] extends { type: 'number' } ? number : never;
    pattern?: T[K] extends { type: 'text' | 'email' } ? RegExp : never;
  };
};

// Generate error messages type
type ErrorMessages<T extends FormSchema> = {
  [K in keyof T as `${string & K}Error`]?: string;
};

// Generate form state type
type FormState<T extends FormSchema> = {
  data: FormData<T>;
  errors: ErrorMessages<T>;
  touched: {
    [K in keyof T]?: boolean;
  };
  isValid: boolean;
  isSubmitting: boolean;
};

// Generate event handler types
type FormEventHandlers<T extends FormSchema> = {
  [K in keyof T as `handle${Capitalize<string & K>}Change`]: (
    value: FormData<T>[K]
  ) => void;
} & {
  handleSubmit: (data: FormData<T>) => void | Promise<void>;
  handleReset: () => void;
  validateField: (fieldName: keyof T) => string | undefined;
};

// Example usage
const userFormSchema: FormSchema = {
  firstName: {
    type: 'text',
    label: 'First Name',
    required: true,
    placeholder: 'Enter your first name'
  },
  lastName: {
    type: 'text',
    label: 'Last Name',
    required: true,
    placeholder: 'Enter your last name'
  },
  email: {
    type: 'email',
    label: 'Email Address',
    required: true,
    placeholder: 'Enter your email'
  },
  age: {
    type: 'number',
    label: 'Age',
    required: false
  },
  country: {
    type: 'select',
    label: 'Country',
    required: true,
    options: [
      { value: 'us', label: 'United States' },
      { value: 'ca', label: 'Canada' },
      { value: 'uk', label: 'United Kingdom' }
    ]
  },
  newsletter: {
    type: 'checkbox',
    label: 'Subscribe to newsletter',
    defaultChecked: false
  }
};

// Generated types
type UserFormData = FormData<typeof userFormSchema>;
// {
//   firstName: string;
//   lastName: string;
//   email: string;
//   age: number;
//   country: string;
//   newsletter: boolean;
// }

type UserFormValidation = ValidationRules<typeof userFormSchema>;
// {
//   firstNameValidation: { required?: boolean; minLength?: number; maxLength?: number; };
//   lastNameValidation: { required?: boolean; minLength?: number; maxLength?: number; };
//   emailValidation: { required?: boolean; pattern?: RegExp; };
//   ageValidation: { required?: boolean; min?: number; max?: number; };
//   countryValidation: { required?: boolean; };
//   newsletterValidation: { required?: boolean; };
// }

type UserFormEventHandlers = FormEventHandlers<typeof userFormSchema>;
// {
//   handleFirstNameChange: (value: string) => void;
//   handleLastNameChange: (value: string) => void;
//   handleEmailChange: (value: string) => void;
//   handleAgeChange: (value: number) => void;
//   handleCountryChange: (value: string) => void;
//   handleNewsletterChange: (value: boolean) => void;
//   handleSubmit: (data: UserFormData) => void | Promise<void>;
//   handleReset: () => void;
//   validateField: (fieldName: keyof typeof userFormSchema) => string | undefined;
// }

// Form builder implementation
class FormBuilder<T extends FormSchema> {
  private schema: T;
  private validationRules: ValidationRules<T>;

  constructor(schema: T, validationRules: ValidationRules<T>) {
    this.schema = schema;
    this.validationRules = validationRules;
  }

  createFormState(): FormState<T> {
    const data = {} as FormData<T>;
    const errors = {} as ErrorMessages<T>;
    const touched = {} as { [K in keyof T]?: boolean };

    // Initialize form data with default values
    for (const [fieldName, field] of Object.entries(this.schema)) {
      if (field.type === 'checkbox') {
        (data as any)[fieldName] = field.defaultChecked || false;
      } else if (field.type === 'number') {
        (data as any)[fieldName] = 0;
      } else {
        (data as any)[fieldName] = '';
      }
    }

    return {
      data,
      errors,
      touched,
      isValid: false,
      isSubmitting: false
    };
  }

  validateField(fieldName: keyof T, value: any): string | undefined {
    const field = this.schema[fieldName];
    const rules = (this.validationRules as any)[`${String(fieldName)}Validation`];

    if (rules?.required && (!value || value === '')) {
      return `${field.label} is required`;
    }

    if (field.type === 'email' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return 'Please enter a valid email address';
    }

    if (rules?.minLength && value && value.length < rules.minLength) {
      return `${field.label} must be at least ${rules.minLength} characters`;
    }

    if (rules?.maxLength && value && value.length > rules.maxLength) {
      return `${field.label} must be no more than ${rules.maxLength} characters`;
    }

    if (rules?.min && field.type === 'number' && value < rules.min) {
      return `${field.label} must be at least ${rules.min}`;
    }

    if (rules?.max && field.type === 'number' && value > rules.max) {
      return `${field.label} must be no more than ${rules.max}`;
    }

    return undefined;
  }
}

// Usage example
const userFormValidation: UserFormValidation = {
  firstNameValidation: { required: true, minLength: 2, maxLength: 50 },
  lastNameValidation: { required: true, minLength: 2, maxLength: 50 },
  emailValidation: { required: true },
  ageValidation: { min: 0, max: 120 },
  countryValidation: { required: true },
  newsletterValidation: {}
};

const userFormBuilder = new FormBuilder(userFormSchema, userFormValidation);
const formState = userFormBuilder.createFormState();

// Type-safe form handling
const handleFirstNameChange = (value: string) => {
  const error = userFormBuilder.validateField('firstName', value);
  // Update form state...
};

const handleSubmit = (data: UserFormData) => {
  console.log('Submitting form data:', data);
  // Submit to API...
};

Benefits of This Approach:

  1. Type Safety: All form data, validation rules, and event handlers are fully typed
  2. IntelliSense: Full autocomplete support for all generated types
  3. Compile-time Validation: TypeScript catches errors at compile time
  4. Reusability: The form builder can work with any schema
  5. Maintainability: Changes to the schema automatically update all related types
  6. Consistency: Generated types ensure consistency across the application

This example demonstrates how mapped types and template literal types can be combined to create powerful, type-safe abstractions that would be impossible to achieve with traditional type definitions alone.