Os utility types do TypeScript fazem parte da biblioteca padrão da linguagem, vêm com cada versão do TypeScript, e a maioria dos desenvolvedores Angular os subutiliza. Eu fiz muitos code reviews ao longo dos anos e o mesmo padrão aparece constantemente: um desenvolvedor escreve uma nova interface que é quase idêntica a uma existente, diferindo apenas em quais campos são obrigatórios ou quais campos estão incluídos. É exatamente para isso que Partial, Pick, Omit e Required existem. Usá-los reduz duplicação, melhora a refatorabilidade e comunica intenção. Aqui está como eles realmente ficam em código Angular.
Partial: Pare de Escrever Interfaces de Update Separadas
Partial torna cada propriedade de um tipo opcional. O caso de uso canônico em Angular são payloads de PATCH e atualizações parciais de formulário. Antes de eu internalizar isso, eu ficava escrevendo interfaces UpdateUser separadas que eram apenas User com tudo opcional. É exatamente isso que Partial faz automaticamente.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface UserProfile {
id: string;
firstName: string;
lastName: string;
email: string;
timezone: string;
plan: 'free' | 'pro' | 'team';
seats: number;
}
@Injectable({ providedIn: 'root' })
export class UserProfileService {
private http = inject(HttpClient);
// No need for a separate UpdateUserProfile interface
updateProfile(id: string, changes: Partial<UserProfile>): Observable<UserProfile> {
return this.http.patch<UserProfile>(`/api/profiles/${id}`, changes);
}
}
// TypeScript enforces that only valid fields are passed
this.userProfileService.updateProfile(id, {
timezone: 'America/Toronto',
plan: 'pro'
});Required: Garanta Completude em uma Fronteira
Required inverte o Partial. Cada propriedade opcional se torna obrigatória. Eu uso em fronteiras onde sei que um objeto parcialmente construído deve estar completo antes de prosseguir. Fábricas de teste são o caso mais comum. Uma coisa que as pessoas erram: Required só remove o ponto de interrogação. Ele não remove null nem undefined do tipo união de uma propriedade. Se um campo é tipado como string | null, Required<T> mantém o null.
interface DraftOrder {
customerId?: string;
totalAmount?: number;
shippingMethod?: string;
itemCount?: number;
}
function validateComplete(draft: DraftOrder): Required<DraftOrder> {
const fields = ['customerId', 'totalAmount', 'shippingMethod', 'itemCount'] as const;
for (const field of fields) {
// Required removes the optional marker, so undefined is the only gap to check
if (draft[field] === undefined) {
throw new Error(`Order is missing required field: ${field}`);
}
}
return draft as Required<DraftOrder>;
}
// In tests, Required<T> enforces complete mock objects
function createMockOrder(): Required<DraftOrder> {
return {
customerId: 'cust-123',
totalAmount: 24900,
shippingMethod: 'standard',
itemCount: 3
};
}Pick: Restrinja os Inputs do Componente ao Que Ele Realmente Usa
Pick cria um novo tipo contendo apenas as chaves que você especifica. Para inputs de componentes, isso é mais informativo do que passar o objeto de domínio completo. Documenta o que o componente realmente lê e impede que componentes acessem campos com os quais não deveriam se preocupar.
import { Component, Input } from '@angular/core';
interface Order {
id: string;
customerName: string;
totalAmount: number;
discountAmount: number | null;
status: 'pending' | 'paid' | 'shipped' | 'cancelled';
placedAt: Date;
shippedAt: Date | null;
fulfilledBy: string | null;
warehouseNotes: string;
weightKg: number;
}
// The card component needs none of the fulfilment fields
type OrderCardData = Pick<
Order,
'id' | 'customerName' | 'totalAmount' | 'status' | 'placedAt'
>;
@Component({ selector: 'app-order-card', standalone: true, template: '' })
export class OrderCardComponent {
@Input({ required: true }) order!: OrderCardData;
}Omit: Padrões de Criação vs Atualização
Omit é o complemento do Pick. Me dê tudo exceto essas chaves. O padrão Angular mais comum são payloads de criação onde campos gerados pelo servidor ainda não existem.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface Order {
id: string;
status: 'pending' | 'paid' | 'shipped';
placedAt: Date;
shippedAt: Date | null;
// These come from the user
customerId: string;
totalAmount: number;
shippingMethod: string;
itemCount: number;
}
// Server generates id, status, placedAt, shippedAt
type CreateOrderPayload = Omit<
Order,
'id' | 'status' | 'placedAt' | 'shippedAt'
>;
@Injectable({ providedIn: 'root' })
export class OrdersService {
private http = inject(HttpClient);
createOrder(payload: CreateOrderPayload): Observable<Order> {
return this.http.post<Order>('/api/orders', payload);
}
}Record: Mapas de Lookup Tipados
Record cria um tipo de objeto com tipos de chave e valor definidos. Eu uso constantemente para objetos de configuração, mapas de status e qualquer estrutura estilo dicionário. É mais seguro que { [key: string]: something } porque as chaves são tipadas.
type OrderStatus = 'pending' | 'paid' | 'packing' | 'shipped' | 'cancelled';
const statusLabels: Record<OrderStatus, string> = {
'pending': 'Pending Payment',
'paid': 'Paid',
'packing': 'Packing',
'shipped': 'Shipped',
'cancelled': 'Cancelled'
};
// TypeScript enforces every status is covered
// Adding a new status to the union forces you to add the label
const statusColors: Record<OrderStatus, string> = {
'pending': '#f59e0b',
'paid': '#3b82f6',
'packing': '#8b5cf6',
'shipped': '#10b981',
'cancelled': '#9ca3af'
};ReturnType e Parameters: Trabalhando com Código Que Você Não Controla
Esses dois são poderosos quando você está trabalhando com funções de terceiros, clientes de API gerados automaticamente ou adaptadores legados onde você não pode controlar as definições de tipo diretamente. ReturnType extrai o que uma função retorna. Parameters extrai o que ela aceita.
import { Injectable, inject } from '@angular/core';
import { Observable, from, catchError } from 'rxjs';
// Working with a legacy adapter you cannot modify
declare function fetchShippingRates(
params: { postalCode: string; weightKg: number; carrier: 'standard' | 'express' }
): Promise<RawRateResponse>;
// Extract the types without duplicating them
type RateParams = Parameters<typeof fetchShippingRates>[0];
type RateResult = Awaited<ReturnType<typeof fetchShippingRates>>;
@Injectable({ providedIn: 'root' })
export class ShippingRatesService {
private errorHandler = inject(ApiErrorHandler);
// Use the extracted types in your Angular service wrapper
getRates(params: RateParams): Observable<RateResult> {
return from(fetchShippingRates(params)).pipe(
catchError(err => this.errorHandler.handle(err))
);
}
}Awaited é a peça que torna isso utilizável. ReturnType de uma função async devolve uma Promise, e Awaited a desembrulha, então o wrapper tipa o valor resolvido em vez da Promise. O caso clássico é um cliente de API gerado automaticamente a partir de uma spec Swagger (OpenAPI). Os tipos gerados nem sempre são o que você precisa para a camada Angular. ReturnType e Parameters permitem construir wrappers sem duplicar as definições de tipo geradas, que divergiriam a cada regeneração da API.
Readonly: Aplique Imutabilidade no Nível do Tipo
Readonly torna cada propriedade não-reatribuível. Com selectors do NgRx e signals, isso reforça o padrão de que código de view nunca deve mutar estado diretamente.
import { Component, inject } from '@angular/core';
import { Store, createSelector } from '@ngrx/store';
export const selectCurrentOrder = createSelector(
selectOrdersState,
(state): Readonly<Order> | null =>
state.currentOrder ? { ...state.currentOrder } : null
);
@Component({ selector: 'app-order-detail', standalone: true, template: '' })
export class OrderDetailComponent {
private store = inject(Store);
// The Readonly type prevents accidental mutation in the view layer
readonly order = this.store.selectSignal(selectCurrentOrder);
// TypeScript error if you try to mutate:
// this.order().status = 'shipped'; // Cannot assign to 'status' because it is a read-only property
}O salto de escrever TypeScript para pensar em TypeScript aparece exatamente nesses lugares: buscar um utility type nativo em vez de escrever uma nova interface do zero.
Comece com Partial, Pick e Omit. Eles aparecem em quase todo serviço e fronteira de componente quando você está prestando atenção. Adicione Record para qualquer estrutura de lookup ou configuração que construir. Avance para ReturnType e Parameters quando estiver encapsulando código que não pode modificar. Isso não é ginástica de tipos avançada. É o kit de ferramentas padrão de qualquer pessoa escrevendo TypeScript mantenível.
Referências
- TypeScript utility types. https://www.typescriptlang.org/docs/handbook/utility-types.html
- Angular @Input decorator. https://angular.dev/api/core/Input