Index signatures e mapped types combinados

1. Fundamentos das Index Signatures

Index signatures permitem definir tipos para objetos com chaves dinâmicas. A sintaxe básica utiliza colchetes para declarar o tipo da chave e o tipo do valor correspondente:

// Sintaxe básica
interface Dictionary {
  [key: string]: unknown;
}

// Restrições de tipo para chave (string, number, symbol)
interface NumberMap {
  [key: number]: string;
}

const map: NumberMap = {
  0: "zero",
  1: "um",
  2: "dois"
};

// Index signatures com tipos union
interface ConfigMap {
  [key: string]: string | number | boolean;
}

// Tipos literais como chave
interface EventMap {
  [key: `on${string}`]: () => void;
}

2. Mapped Types: O Poder da Transformação

Mapped types permitem transformar tipos existentes iterando sobre suas propriedades:

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

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

type NullableUser = Nullable<User>;
// { name: string | null; age: number | null; email: string | null; }

// Mapped types com genéricos e restrições
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

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

3. Combinando Index Signatures com Mapped Types

A verdadeira potência surge quando combinamos index signatures com mapped types para criar tipos dinâmicos complexos:

// Record<K, V> como caso especial
type MyRecord<K extends string | number | symbol, V> = {
  [P in K]: V;
};

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

// Index signatures condicionais
type ConditionalMap<T> = {
  [K in keyof T]: T[K] extends Function ? T[K] : never;
};

interface Service {
  start(): void;
  stop(): void;
  status: string;
}

type ServiceMethods = ConditionalMap<Service>;
// { start: () => void; stop: () => void; status: never; }

4. Manipulação de Chaves com Template Literals

Template literals com as permitem renomear chaves durante o mapeamento:

// Renomeando chaves com prefixo
type WithPrefix<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K];
};

interface Actions {
  click: () => void;
  hover: () => void;
}

type EventActions = WithPrefix<Actions, "on">;
// { onClick: () => void; onHover: () => void; }

// Sistema de eventos com prefixo
type EventHandler<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void;
};

interface Events {
  login: { userId: string };
  logout: void;
  error: { message: string; code: number };
}

type EventHandlers = EventHandler<Events>;
// { onLogin: (payload: { userId: string }) => void; ... }

5. Filtragem e Transformação Condicional

Podemos filtrar propriedades usando as never e combinar com tipos condicionais:

// Filtrando propriedades por tipo
type FilterByType<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  category: string;
}

type StringProperties = FilterByType<Product, string>;
// { name: string; description: string; category: string; }

// Combinando com Pick, Omit e Extract
type PublicAPI<T> = {
  [K in keyof T as K extends `_${string}` ? never : K]: T[K];
};

interface InternalAPI {
  _secret: string;
  _config: object;
  publicMethod(): void;
  data: unknown;
}

type Public = PublicAPI<InternalAPI>;
// { publicMethod(): void; data: unknown; }

6. Casos Avançados: Deep Mapped Types

Mapped types recursivos permitem transformar objetos aninhados:

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

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  logging: {
    level: string;
    transports: string[];
  };
}

type ImmutableConfig = DeepReadonly<Config>;
// Todas as propriedades são readonly, inclusive aninhadas

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

type PartialConfig = DeepPartial<Config>;
// Todas as propriedades são opcionais, inclusive aninhadas

7. Validação e Segurança com Constraint

Restringindo chaves e validando tipos de valor para maior segurança:

// Restringindo chaves com extends keyof any
type SafeDictionary<TKey extends string, TValue> = {
  [K in TKey]: TValue;
};

type AllowedKeys = "name" | "age" | "email";
type UserDict = SafeDictionary<AllowedKeys, string>;
// { name: string; age: string; email: string; }

// Dictionary com validação de tipo de valor
type TypedDictionary<TKey extends string | number | symbol, TValue> = {
  [K in TKey]: TValue extends object
    ? { readonly [P in keyof TValue]: TValue[P] }
    : TValue;
};

interface Settings {
  theme: "light" | "dark";
  language: string;
  notifications: boolean;
}

type ImmutableSettings = TypedDictionary<keyof Settings, Settings[keyof Settings]>;
// { theme: { readonly theme: "light" | "dark" }; language: { readonly language: string }; ... }

8. Padrões Práticos e Boas Práticas

Exemplos reais combinando todas as técnicas:

// Sistema de configuração tipado com fallback
type ConfigSchema = {
  api: {
    baseUrl: string;
    timeout: number;
    retries: number;
  };
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
};

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

type UserConfig = DeepPartial<ConfigSchema>;

// Função que mescla configuração com defaults
function mergeConfig<T extends object>(
  defaults: T,
  overrides: DeepPartial<T>
): T {
  const result = { ...defaults };
  for (const key in overrides) {
    if (overrides[key] && typeof overrides[key] === 'object') {
      result[key] = mergeConfig(defaults[key], overrides[key]);
    } else if (overrides[key] !== undefined) {
      result[key] = overrides[key] as any;
    }
  }
  return result;
}

// Uso com as const e satisfies
const defaultConfig = {
  api: {
    baseUrl: "https://api.example.com",
    timeout: 5000,
    retries: 3
  },
  features: {
    darkMode: false,
    analytics: true
  }
} as const satisfies ConfigSchema;

const userOverrides = {
  api: {
    timeout: 10000
  }
} satisfies DeepPartial<ConfigSchema>;

const finalConfig = mergeConfig(defaultConfig, userOverrides);

Boas práticas:
- Evite index signatures quando as chaves são conhecidas em tempo de compilação
- Prefira mapped types para transformações previsíveis
- Use as const para inferir tipos literais corretamente
- Combine satisfies para validar sem alterar o tipo inferido
- Documente tipos complexos com comentários JSDoc

Referências