Voltar para o blog
Desenvolvimento Web

TypeScript: Patterns Avançados para Aplicações Robustas

Domine patterns avançados de TypeScript para escrever código type-safe e escalável

Foto de Vinicius Mendes
Especialista em IA Aplicada | Full Stack Engineer | UX/UI Designer
20 min de leitura
Imagem de capa do artigo: TypeScript: Patterns Avançados para Aplicações Robustas

TypeScript: Patterns Avançados para Aplicações Robustas

O TypeScript vai muito além de adicionar tipos ao JavaScript. Quando dominamos seus recursos avançados, conseguimos criar código mais seguro, expressivo e fácil de manter. Neste guia, vamos explorar patterns avançados que vão elevar seu código TypeScript ao próximo nível.

TypeScript - Patterns Avançados

Por que Patterns Avançados?

Patterns avançados de TypeScript nos ajudam a:

Benefício Descrição
Type Safety Detectar erros em tempo de compilação
Autocompletion IDE sugere opções corretas
Refactoring Mudanças propagam automaticamente
Documentação Tipos servem como documentação viva
Manutenibilidade Código mais fácil de entender

"TypeScript é JavaScript que escala. Os patterns avançados são o que fazem essa escalabilidade possível." - Anders Hejlsberg

Utility Types Avançados

Criando Utility Types Customizados

typescript
// DeepPartial - torna todas as propriedades opcionais recursivamente
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Exemplo de uso
interface Config {
  api: {
    baseUrl: string;
    timeout: number;
    headers: {
      authorization: string;
      contentType: string;
    };
  };
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

// Agora podemos fazer updates parciais profundos
function updateConfig(config: Config, updates: DeepPartial<Config>): Config {
  return deepMerge(config, updates);
}

updateConfig(defaultConfig, {
  api: {
    timeout: 5000, // Só atualiza timeout
  },
});

DeepReadonly

typescript
// DeepReadonly - imutabilidade profunda
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P];
};

// Uso com estado imutável
const state: DeepReadonly<AppState> = {
  user: {
    name: 'John',
    preferences: {
      theme: 'dark',
    },
  },
};

// ❌ Erro de compilação!
state.user.preferences.theme = 'light';

NonNullableDeep

typescript
// Remove null e undefined recursivamente
type NonNullableDeep<T> = {
  [P in keyof T]-?: NonNullableDeep<NonNullable<T[P]>>;
};

interface ApiResponse {
  data?: {
    user?: {
      name?: string;
      email?: string;
    } | null;
  } | null;
}

// Após validação, garantimos que tudo existe
type ValidatedResponse = NonNullableDeep<ApiResponse>;
// { data: { user: { name: string; email: string } } }

Template Literal Types

O TypeScript 4.1+ introduziu Template Literal Types, permitindo manipulação de strings em nível de tipo:

Event Handlers Tipados

typescript
// Gerar tipos de eventos automaticamente
type EventName = 'click' | 'focus' | 'blur' | 'change';
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur" | "onChange"

// Usando para criar props de componentes
type EventHandlers = {
  [K in EventHandler]?: (event: Event) => void;
};

interface ButtonProps extends EventHandlers {
  label: string;
  variant: 'primary' | 'secondary';
}

API Routes Tipadas

typescript
// Definir rotas de API com parâmetros tipados
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

type ApiRoute<
  Method extends HttpMethod,
  Path extends string
> = `${Method} ${Path}`;

// Rotas específicas
type UserRoutes =
  | ApiRoute<'GET', '/users'>
  | ApiRoute<'GET', '/users/:id'>
  | ApiRoute<'POST', '/users'>
  | ApiRoute<'PUT', '/users/:id'>
  | ApiRoute<'DELETE', '/users/:id'>;

// Extrair parâmetros da rota
type ExtractParams<T extends string> =
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${infer _Start}:${infer Param}`
    ? Param
    : never;

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

Conditional Types Avançados

Type Inference com infer

typescript
// Extrair tipo de retorno de função async
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

async function fetchUser(): Promise<{ id: number; name: string }> {
  return { id: 1, name: 'John' };
}

type User = UnwrapPromise<ReturnType<typeof fetchUser>>;
// { id: number; name: string }

// Extrair tipo de elementos de array
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type Item = ArrayElement<string[]>; // string

Distributive Conditional Types

typescript
// Filtrar tipos de uma union
type FilterByType<T, U> = T extends U ? T : never;

type Mixed = string | number | boolean | null | undefined;
type OnlyStringsAndNumbers = FilterByType<Mixed, string | number>;
// string | number

// Excluir tipos de uma union (similar ao Exclude built-in)
type ExcludeType<T, U> = T extends U ? never : T;

type NoNullOrUndefined = ExcludeType<Mixed, null | undefined>;
// string | number | boolean

Mapped Types com Modificadores

Criando Builders Tipados

typescript
// Builder pattern com tipos
type Builder<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => Builder<T>;
} & {
  build: () => T;
};

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

// O tipo Builder<UserConfig> terá:
// setName(value: string): Builder<UserConfig>
// setEmail(value: string): Builder<UserConfig>
// setAge(value: number): Builder<UserConfig>
// build(): UserConfig

function createBuilder<T>(): Builder<T> {
  const data: Partial<T> = {};

  const builder = new Proxy({} as Builder<T>, {
    get(_, prop: string) {
      if (prop === 'build') {
        return () => data as T;
      }
      if (prop.startsWith('set')) {
        const key = prop.slice(3).toLowerCase() as keyof T;
        return (value: T[keyof T]) => {
          data[key] = value;
          return builder;
        };
      }
    },
  });

  return builder;
}

// Uso
const user = createBuilder<UserConfig>()
  .setName('John')
  .setEmail('john@example.com')
  .setAge(30)
  .build();

Getters e Setters Automáticos

typescript
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;
};

type Accessors<T> = Getters<T> & Setters<T>;

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

type StateAccessors = Accessors<State>;
// {
//   getCount: () => number;
//   setCount: (value: number) => void;
//   getName: () => string;
//   setName: (value: string) => void;
// }

Branded Types

Branded types adicionam segurança semântica ao código:

typescript
// Definir branded types
declare const brand: unique symbol;

type Brand<T, B> = T & { [brand]: B };

// Criar tipos específicos
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
type Email = Brand<string, 'Email'>;

// Funções para criar instâncias validadas
function createUserId(id: string): UserId {
  if (!id.startsWith('user_')) {
    throw new Error('Invalid user ID format');
  }
  return id as UserId;
}

function createEmail(email: string): Email {
  if (!email.includes('@')) {
    throw new Error('Invalid email format');
  }
  return email as Email;
}

// Agora o TypeScript previne erros
function getUser(id: UserId): Promise<User> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

function getPost(id: PostId): Promise<Post> {
  return fetch(`/api/posts/${id}`).then(r => r.json());
}

const userId = createUserId('user_123');
const postId = 'post_456' as PostId;

getUser(userId); // ✅ OK
getUser(postId); // ❌ Erro! PostId não é UserId

Discriminated Unions

Pattern essencial para state machines e handling de diferentes casos:

typescript
// Estado de requisição assíncrona
type AsyncState<T, E = Error> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: E };

// Uso
function useAsync<T>(): AsyncState<T> {
  // implementação...
}

function renderUser(state: AsyncState<User>) {
  switch (state.status) {
    case 'idle':
      return <p>Click to load</p>;
    case 'loading':
      return <Spinner />;
    case 'success':
      // TypeScript sabe que state.data existe aqui!
      return <UserCard user={state.data} />;
    case 'error':
      // TypeScript sabe que state.error existe aqui!
      return <ErrorMessage error={state.error} />;
  }
}

Exhaustive Check

typescript
// Garantir que todos os casos são tratados
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'RESET'; payload: number };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    case 'RESET':
      return action.payload;
    default:
      // Se adicionar novo tipo de action e esquecer de tratar,
      // TypeScript vai reclamar aqui!
      return assertNever(action);
  }
}

Function Overloading

typescript
// Overloads para diferentes tipos de input/output
function parse(input: string): object;
function parse(input: string, reviver: (key: string, value: unknown) => unknown): object;
function parse(input: Buffer): object;
function parse(
  input: string | Buffer,
  reviver?: (key: string, value: unknown) => unknown
): object {
  const str = typeof input === 'string' ? input : input.toString();
  return JSON.parse(str, reviver);
}

// TypeScript infere o tipo correto baseado no input
const obj1 = parse('{"name": "John"}'); // objeto
const obj2 = parse(Buffer.from('{"name": "John"}')); // objeto

Checklist de Boas Práticas

Configuração

  • Habilitar strict mode no tsconfig.json
  • Usar noUncheckedIndexedAccess
  • Configurar paths aliases (@/)
  • Setup de testes com tipos

Código

  • Preferir interfaces para objetos
  • Usar type para unions e intersections
  • Evitar any - usar unknown quando necessário
  • Usar const assertions para literais
  • Documentar tipos complexos com JSDoc

Patterns

  • Usar discriminated unions para estados
  • Branded types para IDs e valores semânticos
  • Result type para error handling
  • Builder pattern para objetos complexos

Conclusão

Dominar patterns avançados de TypeScript transforma a forma como você escreve código. O investimento em aprender esses conceitos se paga rapidamente em forma de menos bugs, melhor autocompletion e código mais fácil de manter.

Recursos Adicionais

Documentação:

Ferramentas:

  • ts-toolbelt - Utility types avançados
  • zod - Schema validation com inferência de tipos

Este guia é atualizado conforme novas features do TypeScript são lançadas.

Tags

  • TypeScript
  • JavaScript
  • Patterns
  • Types
  • Frontend
  • Backend
  • Clean Code

Continue explorando outros conteúdos que podem te interessar