interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// 1. Omit & Pick pentru API DTOs
type CreateUserDTO = Omit<User, 'id' | 'createdAt'>;
type UserHeaderInfo = Pick<User, 'name' | 'email'>;
// 2. Partial pentru patch/update
type UpdateUserDTO = Partial<CreateUserDTO>;
// 3. Inferență din funcții existente
async function fetchUserProfile(userId: string) {
return {
user: {} as User,
token: 'jwt-token-string',
permissions: ['read', 'write']
};
}
type ProfileResponse = ReturnType<typeof fetchUserProfile>; // Promise<{ user: User... }>Salutare. Acum ceva timp am preluat un codebase de la un client unde aveau 14 interfețe diferite pentru aceeași entitate de User. Am șters aproape 300 de linii de cod inutil în prima săptămână doar folosind utility types de bază.
Dacă încă scrii interfețe manuale pentru DTO-uri, formulare sau răspunsuri de API, pierzi timp și creezi bug-uri de sincronizare. Hai să vedem cum folosesc eu Pick, Omit, Partial, Required și ReturnType în producție.
Pick și Omit: Când vrei doar o parte din entitate
La un proiect de e-commerce cu 12k produse la care am lucrat, entitatea Product avea vreo 25 de câmpuri: de la id și created_at până la stocuri, variante și metadate SEO. Când afișezi un card simplu în listing, nu ai nevoie de tot monstruozitatea asta.
În loc să creezi o interfață nouă ProductCardProps de la zero, folosești Pick<Product, 'id' | 'title' | 'price' | 'thumbnail'>. Dintr-o dată, dacă schimbi tipul price din number în MoneyObject, se actualizează peste tot automat.
Invers, când creezi un produs nou via POST request, ai nevoie de aproape toate câmpurile, mai puțin id și created_at care sunt generate de baza de date. Aici intervine Omit<Product, 'id' | 'created_at'>.
Partial și Required: Formulare și setări default
Formularele de editare sunt coșmarul tipizării dacă nu le gestionezi de la început. La un API de profil, utilizatorul poate schimba doar numele, doar avatarul sau ambele. Dacă pasezi obiectul de update, Partial<User> face toate proprietățile opționale (?).
Required face exact opusul. Îl folosesc des la configurările aplicației. Utilizatorul poate furniza un obiect de opțiuni parțial, dar în interiorul modulelor mele combin opțiunile primite cu un default static și le trec prin Required<AppConfig> ca să fiu sigur că nu am undefined la runtime.
ReturnType: Când librăria terță nu-și exportă tipurile
Am pățit-o recent cu un SDK intern de plăți. Funcția lor de createTransaction returna un obiect complex, dar librăria nu exporta interfața pentru acel rezultat. În loc să fac copy-paste la 40 de câmpuri și să sper că nu se schimbă nimic la următorul update, am rezolvat scurt:
type TransactionResult = ReturnType<typeof createTransaction>;
Este un life-saver extrem de util când lucrezi cu ORM-uri gen Prisma sau ksqlDB, unde interogările complexe generează tipuri anonime enorme.
Unde devine urât (Trade-offs)
Utility types sunt excelente, dar au o capcană mare: lizibilitatea. Când vezi în cod ceva de genul Partial<Omit<Pick<User, 'id' | 'name' | 'email'>, 'email'>>, e momentul să te oprești. Nimeni din echipă nu va înțelege ce se întâmplă acolo fără să piardă 2 minute să decodifice tipul.
De asemenea, refactorizările mari devin mai greu de urmărit dacă abuzezi de ReturnType peste tot. Pierzi contractul explicit (explicit type annotation) și te bazezi 100% pe inferență. Dacă cineva modifică din greșeală ce returnează o funcție de bază, eroarea de TS poate apărea la 10 fișiere distanță, într-un loc total lipsit de sens.
Voi cât de adânc compuneți utility types în proiectele voastre? Aveți o limită de nesting stabilită în ESLint sau mergeți pe feeling?