Retour au Blog
Angular Development22 mai 202410 min de lecture

Les types utilitaires TypeScript que tout développeur Angular devrait connaître

DR

Doug Ribeiro

10 min de lecture

Les types utilitaires TypeScript sont intégrés à la bibliothèque standard du langage, ils sont livrés avec chaque version de TypeScript, et la plupart des développeurs Angular les sous-utilisent. J'ai fait beaucoup de revues de code au fil des ans et le même pattern apparaît constamment : un développeur écrit une nouvelle interface qui est presque identique à une existante, ne différant que par quels champs sont requis ou quels champs sont inclus. C'est exactement ce pour quoi Partial, Pick, Omit et Required existent. Les utiliser réduit la duplication, améliore la refactorisabilité et communique l'intention. Voici à quoi ils ressemblent réellement dans du code Angular.

Remodeler un type existantPartial<T>. Tous les champs opt…Required<T>. Tous les champs re…Readonly<T>. ImmuablePick<T, K>. Sélectionner des ch…Omit<T, K>. Retirer des champsConstruire un type à partir d'autre choseRecord<K, V>. Table de correspo…ReturnType<T>. Type de retour d…Parameters<T>. Types des paramè…Awaited<T>. Extrait le type d''…VS

Partial : arrêtez d'écrire des interfaces Update séparées

Partial rend chaque propriété d'un type optionnelle. Le cas d'usage Angular canonique est les payloads PATCH et les mises à jour partielles de formulaire. Avant d'intérioriser ça, j'écrivais des interfaces UpdateUser séparées qui étaient juste User avec tout en optionnel. C'est exactement ce que Partial fait automatiquement.

typescript
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 : imposer la complétude à une frontière

Required inverse Partial. Chaque propriété optionnelle devient non-optionnelle. Je l'utilise aux frontières où je sais qu'un objet partiellement construit devrait être complet avant de continuer. Les factories de test sont le cas le plus courant. Un point sur lequel les gens se trompent : Required ne retire que le point d'interrogation. Il ne retire pas null ou undefined du type union d'une propriété. Si un champ est typé string | null, Required<T> garde le null.

typescript
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 : restreindre les inputs de composant à ce qu'ils utilisent réellement

Pick crée un nouveau type contenant uniquement les clés que vous spécifiez. Pour les inputs de composant, c'est plus informatif que de passer l'objet domaine complet. Ça documente ce que le composant lit réellement et empêche les composants d'accéder à des champs dont ils ne devraient pas se soucier.

typescript
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 : patterns Create vs Update

Omit est le complément de Pick. Donnez-moi tout sauf ces clés. Le pattern Angular le plus courant est les payloads de création où les champs générés par le serveur n'existent pas encore.

typescript
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 : maps de recherche typées

Record crée un type objet avec des types de clé et valeur définis. Je l'utilise constamment pour les objets de configuration, les maps de statut et toute structure de type dictionnaire. C'est plus sûr que { [key: string]: something } parce que les clés sont typées.

typescript
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 et Parameters : travailler avec du code que vous ne possédez pas

Ces deux-là sont puissants quand vous travaillez avec des fonctions tierces, des clients API auto-générés, ou des adaptateurs legacy où vous ne pouvez pas contrôler les définitions de types directement. ReturnType extrait ce qu'une fonction retourne. Parameters extrait ce qu'elle accepte.

typescript
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 est la pièce qui rend tout ça utilisable. ReturnType d'une fonction asynchrone vous donne une Promise, et Awaited la déballe, si bien que le wrapper type la valeur résolue plutôt que la Promise. Le cas classique est un client API généré automatiquement à partir d'une spec Swagger (OpenAPI). Les types générés ne sont pas toujours ce dont vous avez besoin pour la couche Angular. ReturnType et Parameters vous permettent de construire des wrappers sans dupliquer les définitions de types générées, qui dériveraient à chaque régénération de l'API.

Readonly : imposer l'immutabilité au niveau du type

Readonly rend chaque propriété non-réassignable. Avec les selectors NgRx et les signals, ça renforce le pattern que le code de vue ne devrait jamais muter l'état directement.

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

Le passage de l'écriture de TypeScript à la pensée en TypeScript se manifeste exactement dans ces endroits : prendre un utilitaire intégré plutôt qu'écrire une nouvelle interface de zéro.

Commencez par Partial, Pick et Omit. Ils apparaissent dans presque chaque frontière de service et de composant une fois que vous les cherchez. Ajoutez Record pour toute structure de recherche ou de configuration que vous construisez. Graduez vers ReturnType et Parameters quand vous encapsulez du code que vous ne pouvez pas modifier. Ce ne sont pas des acrobaties de types avancées. C'est la boîte à outils standard de quiconque écrit du TypeScript maintenable.

Références

TypeScriptAngularFrontendBest Practices