Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptGenerics Avançado

Conditional & Mapped Types

Ternários em tipos, infer, mapped types e tipos recursivos — programação de tipos completa.

Conditional & Mapped Types

Conditional types e mapped types são o que transforma TypeScript de "JavaScript com anotações" em uma linguagem de tipos Turing-complete. Com eles, você pode derivar, transformar e validar tipos de formas que nenhum outro sistema de tipos mainstream permite.


Conditional Types — extends ? :

Um conditional type é um ternário no nível de tipos. Se A estende B, retorna X; senão, retorna Y.

// Sintaxe: T extends U ? X : Y
type IsString<T> = T extends string ? 'sim' : 'não';

type Test1 = IsString<string>;  // 'sim'
type Test2 = IsString<number>;  // 'não'
type Test3 = IsString<'hello'>; // 'sim' (string literal extends string)

Distribuição sobre unions

Quando T é uma union, o conditional type distribui — aplica em cada membro separadamente:

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// string[] | number[]  (NÃO Array<string | number>)

// Para NÃO distribuir, envolva em colchetes:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNonDist<string | number>;
// Array<string | number>

Extração de tipo de promise

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<User>>;   // User
type C = UnwrapPromise<number>;          // number (não é Promise, retorna como está)

infer — Extrair tipos de dentro de outros tipos

infer permite declarar uma variável de tipo dentro de um conditional e extraí-la de estruturas complexas.

Extrair tipo de retorno de função

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = GetReturnType<(x: number) => string>;  // string
type B = GetReturnType<typeof getUser>;          // User (se getUser retornar User)

Extrair tipo de array

type ElementType<T> = T extends (infer U)[] ? U : never;

type A = ElementType<string[]>;      // string
type B = ElementType<number[]>;      // number
type C = ElementType<User[]>;        // User
type D = ElementType<string>;        // string (não é array — retorna como está)

Extrair parâmetros de Promise

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
// Recursivo — lida com Promise<Promise<string>>

type A = Awaited<Promise<Promise<number>>>; // number

Extrair primeiro e último de tuple

type Head<T> = T extends [infer First, ...any[]] ? First : never;
type Tail<T> = T extends [any, ...infer Rest] ? Rest : never;
type Last<T> = T extends [...any[], infer L] ? L : never;

type A = Head<[1, 2, 3]>;  // 1
type B = Tail<[1, 2, 3]>;  // [2, 3]
type C = Last<[1, 2, 3]>;  // 3

Extrair tipo de evento DOM

type EventHandler<T extends string> =
  T extends `on${infer Event}` ? Lowercase<Event> : never;

type A = EventHandler<'onClick'>;     // 'click'
type B = EventHandler<'onMouseEnter'>; // 'mouseenter'
type C = EventHandler<'onSubmit'>;     // 'submit'

Mapped Types — Transformar cada propriedade

Um mapped type itera sobre as chaves de um tipo existente e aplica uma transformação.

Sintaxe básica

// { [K in keyof T]: transformação }
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type ReadonlyUser = Readonly<User>;
// { readonly id: string; readonly name: string; readonly email: string; ... }

Adicionar/modificar propriedades

// Adiciona campo 'loading' a cada propriedade
type WithLoading<T> = {
  [K in keyof T]: {
    value: T[K];
    loading: boolean;
  };
};

type AsyncUser = WithLoading<Pick<User, 'name' | 'email'>>;
// {
//   name: { value: string; loading: boolean };
//   email: { value: string; loading: boolean };
// }

Remapping com as

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

type UserSetters = Setters<Pick<User, 'name' | 'email'>>;
// {
//   setName: (value: string) => void;
//   setEmail: (value: string) => void;
// }

Filtrar propriedades

// Remove propriedades que não são funções
type FunctionProperties<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

// Remove propriedades que são funções
type DataProperties<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

interface UserService {
  id: string;
  name: string;
  getUser: (id: string) => User;
  createUser: (data: CreateUserDto) => User;
}

type Methods = FunctionProperties<UserService>;
// { getUser: (id: string) => User; createUser: (data: CreateUserDto) => User; }

type Data = DataProperties<UserService>;
// { id: string; name: string; }

Tipos recursivos

Conditional + mapped types permitem recursão — tipos que se referenciam.

DeepReadonly

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

interface Config {
  server: {
    host: string;
    ports: number[];
  };
}

type FrozenConfig = DeepReadonly<Config>;
// {
//   readonly server: {
//     readonly host: string;
//     readonly ports: readonly number[];
//   };
// }

DeepRequired (remove todos os optionals e nullables)

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

interface FormState {
  name?: string;
  address?: {
    street?: string;
    city?: string;
  };
}

type CompleteForm = DeepRequired<FormState>;
// { name: string; address: { street: string; city: string; } }

Path-based access

type Get<T, K extends string> = K extends `${infer Head}.${infer Tail}`
  ? Head extends keyof T
    ? Get<T[Head], Tail>
    : never
  : K extends keyof T
    ? T[K]
    : never;

interface App {
  user: {
    profile: {
      name: string;
      avatar: { url: string };
    };
    settings: { theme: 'light' | 'dark' };
  };
}

type Name = Get<App, 'user.profile.name'>;        // string
type Theme = Get<App, 'user.settings.theme'>;      // 'light' | 'dark'
type Url = Get<App, 'user.profile.avatar.url'>;    // string

Exemplo full-stack — API response builder

// Tipos que derivam de definições de API
type APIResponse<T> = {
  data: T;
  meta: {
    total: number;
    page: number;
    perPage: number;
  };
};

type PaginatedParams = {
  page?: number;
  perPage?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
};

// Gera o tipo de parâmetros automaticamente a partir da rota
type RouteParams<R> = R extends `/api/${string}/:${infer Param}`
  ? Record<Param, string>
  : Record<string, never>;

type UserParams = RouteParams<'/api/users/:id'>;  // Record<'id', string>
type OrderParams = RouteParams<'/api/orders/:orderId/items/:itemId'>;
// Record<'orderId' | 'itemId', string>

Referências

On this page