LÄs upp kraften i TypeScript Mapped Types för dynamiska objektransformationer och flexibla egenskapsmodifieringar, vilket förbÀttrar kodÄteranvÀndning och typsÀkerhet.
TypeScript Mapped Types: BemÀstra objektransformation och egenskapsmodifiering
I det stÀndigt förÀnderliga landskapet av mjukvaruutveckling Àr robusta typsystem avgörande för att bygga underhÄllbara, skalbara och pÄlitliga applikationer. TypeScript, med sin kraftfulla typinferens och avancerade funktioner, har blivit ett oumbÀrligt verktyg för utvecklare vÀrlden över. Bland dess mest potenta funktioner finns Mapped Types, en sofistikerad mekanism som gör det möjligt för oss att transformera befintliga objekttyper till nya. Detta blogginlÀgg kommer att djupdyka i vÀrlden av TypeScript Mapped Types, utforska deras grundlÀggande koncept, praktiska tillÀmpningar och hur de ger utvecklare möjlighet att elegant hantera objektransformationer och egenskapsmodifieringar.
FörstÄ kÀrnkonceptet bakom Mapped Types
I grunden Àr en Mapped Type ett sÀtt att skapa nya typer genom att iterera över egenskaperna hos en befintlig typ. TÀnk pÄ det som en loop för typer. För varje egenskap i den ursprungliga typen kan du tillÀmpa en transformation pÄ dess nyckel, dess vÀrde, eller bÄda. Detta öppnar upp ett stort antal möjligheter för att generera nya typdefinitioner baserat pÄ befintliga, utan manuell repetition.
Den grundlÀggande syntaxen för en Mapped Type involverar en { [P in K]: T }-struktur, dÀr:
P: Representerar namnet pÄ egenskapen som itereras över.in K: Detta Àr den avgörande delen, som anger attPkommer att anta varje nyckel frÄn typenK(vilket typiskt Àr en union av strÀngliteraler eller en keyof-typ).T: Definierar typen av vÀrdet för egenskapenPi den nya typen.
LÄt oss börja med en enkel illustration. FörestÀll dig att du har ett objekt som representerar anvÀndardata, och du vill skapa en ny typ dÀr alla egenskaper Àr valfria. Detta Àr ett vanligt scenario, till exempel nÀr du bygger konfigurationsobjekt eller nÀr du implementerar partiella uppdateringar.
Exempel 1: Göra alla egenskaper valfria
Betrakta denna bas-typ:
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
};
Vi kan skapa en ny typ, OptionalUser, dÀr alla dessa egenskaper Àr valfria med hjÀlp av en Mapped Type:
type OptionalUser = {
[P in keyof User]?: User[P];
};
LÄt oss bryta ner detta:
keyof User: Detta genererar en union av nycklarna förUser-typen (t.ex.'id' | 'name' | 'email' | 'isActive').P in keyof User: Detta itererar över varje nyckel i unionen.?: Detta Àr modifieraren som gör egenskapen valfri.User[P]: Detta Àr en uppslagstyp. För varje nyckelPhÀmtar den motsvarande vÀrdetypen frÄn den ursprungligaUser-typen.
Den resulterande OptionalUser-typen skulle se ut sÄ hÀr:
{
id?: number;
name?: string;
email?: string;
isActive?: boolean;
}
Detta Àr otroligt kraftfullt. IstÀllet för att manuellt omdefiniera varje egenskap med ett ?, har vi genererat typen dynamiskt. Denna princip kan utökas för att skapa mÄnga andra hjÀlptyper.
Vanliga egenskapsmodifierare i Mapped Types
Mapped Types handlar inte bara om att göra egenskaper valfria. De tillÄter dig att tillÀmpa olika modifierare pÄ egenskaperna i den resulterande typen. De vanligaste inkluderar:
- Valfrihet: LĂ€gga till eller ta bort
?-modifieraren. - Endast lÀsbar: LÀgga till eller ta bort
readonly-modifieraren. - Nullbarhet/Icke-nullbarhet: LĂ€gga till eller ta bort
| nulleller| undefined.
Exempel 2: Skapa en endast lÀsbar version av en typ
Liknande att göra egenskaper valfria, kan vi skapa en ReadonlyUser-typ:
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
Detta kommer att producera:
{
readonly id: number;
readonly name: string;
readonly email: string;
readonly isActive: boolean;
}
Detta Àr oerhört anvÀndbart för att sÀkerstÀlla att vissa datastrukturer, nÀr de vÀl har skapats, inte kan muteras, vilket Àr en grundlÀggande princip för att bygga robusta, förutsÀgbara system, sÀrskilt i samtidiga miljöer eller nÀr man arbetar med immuterbara datamönster som Àr populÀra i funktionella programmeringsparadigmer som antagits av mÄnga internationella utvecklingsteam.
Exempel 3: Kombinera valfrihet och endast lÀsbar
Vi kan kombinera modifierare. Till exempel en typ dÀr egenskaper Àr bÄde valfria och endast lÀsbar:
type OptionalReadonlyUser = {
readonly [P in keyof User]?: User[P];
};
Detta resulterar i:
{
readonly id?: number;
readonly name?: string;
readonly email?: string;
readonly isActive?: boolean;
}
Ta bort modifierare med Mapped Types
Vad hÀnder om du vill *ta bort* en modifierare? TypeScript tillÄter detta genom syntaxen -? och -readonly inom Mapped Types. Detta Àr sÀrskilt kraftfullt nÀr man arbetar med befintliga hjÀlptyper eller komplexa typkompositioner.
LÄt oss sÀga att du har en Partial<T>-typ (som Àr inbyggd och gör alla egenskaper valfria), och du vill skapa en typ som Àr densamma som Partial<T> men dÀr alla egenskaper görs obligatoriska igen.
type Mandatory<T> = {
-?: T extends object ? T[keyof T] : never;
};
type FullyPopulatedUser = Mandatory<Partial<User>>;
Detta verkar kontraintuitivt. LÄt oss analysera det:
Partial<User> Àr ekvivalent med vÄr OptionalUser. Nu vill vi göra dess egenskaper obligatoriska. Syntaxen -? tar bort den valfria modifieraren.
Ett mer direkt sÀtt att uppnÄ detta, utan att förlita sig pÄ Partial först, Àr att helt enkelt ta den ursprungliga typen och göra den obligatorisk om den var valfri:
type MakeMandatory<T> = {
-?: T;
};
type MandatoryUser = MakeMandatory<OptionalUser>;
Detta kommer korrekt att ÄterstÀlla OptionalUser tillbaka till den ursprungliga User-typens struktur (alla egenskaper nÀrvarande och obligatoriska).
PÄ samma sÀtt, för att ta bort readonly-modifieraren:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type MutableUser = Mutable<ReadonlyUser>;
MutableUser kommer att vara ekvivalent med den ursprungliga User-typen, men dess egenskaper kommer inte att vara endast lÀsbar.
Nullbarhet och undefinierbarhet
Du kan ocksÄ kontrollera nullbarhet. Till exempel, för att sÀkerstÀlla att alla egenskaper Àr definitivt icke-nullbara:
type NonNullableValues<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};
interface MaybeNull {
name: string | null;
age: number | undefined;
}
type DefiniteValues = NonNullableValues<MaybeNull>;
// type DefiniteValues = {
// name: string;
// age: number;
// }
HÀr sÀkerstÀller -? att egenskaperna inte Àr valfria, och NonNullable<T[P]> tar bort null och undefined frÄn vÀrdetypen.
Transformera egenskapsnycklar
Mapped Types Àr otroligt mÄngsidiga, och de stannar inte bara vid att modifiera vÀrden eller modifierare. Du kan ocksÄ transformera nycklarna i en objekttyp. Det Àr hÀr Mapped Types verkligen lyser i komplexa scenarier.
Exempel 4: Prefixera egenskapsnycklar
Anta att du vill skapa en ny typ dÀr alla egenskaper i en befintlig typ har ett specifikt prefix. Detta kan vara anvÀndbart för namnutrymmen eller för att generera variationer av datastrukturer.
type Prefixed<T, Prefix extends string> = {
[P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};
type OriginalConfig = {
timeout: number;
retries: number;
};
type PrefixedConfig = Prefixed<OriginalConfig, 'app'>;
// type PrefixedConfig = {
// appTimeout: number;
// appRetries: number;
// }
LÄt oss analysera nyckeltransformationen:
P in keyof T: Itererar fortfarande över de ursprungliga nycklarna.as `${Prefix}${Capitalize<string & P>}`: Detta Àr klausulen för nyckelomkartning.`${Prefix}${...}`: Detta anvÀnder mallstrÀngstyper för att konstruera det nya nyckelnamnet genom att sammanfoga den angivnaPrefixmed det transformerade egenskapsnamnet.Capitalize<string & P>: Detta Àr ett vanligt mönster för att sÀkerstÀlla att egenskapens namnPbehandlas som en strÀng och sedan versaliseras. Vi anvÀnderstring & Pför att korsaPmedstring, vilket sÀkerstÀller att TypeScript behandlar det som en strÀngtyp, vilket Àr nödvÀndigt förCapitalize.
Detta exempel visar hur du dynamiskt kan byta namn pÄ egenskaper baserat pÄ befintliga, en kraftfull teknik för att upprÀtthÄlla konsekvens över olika lager av en applikation eller vid integration med externa system som har specifika namngivningskonventioner.
Exempel 5: Filtrera egenskaper
Vad hÀnder om du bara vill inkludera egenskaper som uppfyller ett visst villkor? Detta kan uppnÄs genom att kombinera Mapped Types med Conditional Types och as-klausulen för nyckelomkartning, ofta för att filtrera bort egenskaper.
type OnlyStrings<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
interface MixedData {
name: string;
age: number;
city: string;
isActive: boolean;
}
type StringOnlyData = OnlyStrings<MixedData>;
// type StringOnlyData = {
// name: string;
// city: string;
// }
I det hÀr fallet:
T[P] extends string ? P : never: För varje egenskapPkontrollerar vi om dess vÀrdetyp (T[P]) kan tilldelas tillstring.- Om det Àr en strÀng behÄlls nyckeln
P. - Om det inte Àr en strÀng mappas den till
never. NÀr en nyckel mappas tillnevertas den effektivt bort frÄn den resulterande objekttypen.
Denna teknik Àr ovÀrderlig för att skapa mer specifika typer frÄn bredare, till exempel att extrahera endast konfigurationsinstÀllningar som Àr av en viss typ, eller att separera datafÀlt efter deras natur.
Exempel 6: Transformera nycklar till en annan form
Du kan ocksÄ transformera nycklar till helt andra typer av nycklar, till exempel att omvandla strÀngnycklar till nummer, eller vice versa, Àven om detta Àr mindre vanligt för direkt objektmanipulering och mer för avancerad typ-nivÄprogrammering.
ĂvervĂ€g att omvandla strĂ€ngnycklar till en union av strĂ€ngliteraler och sedan anvĂ€nda det som bas för en ny typ. Ăven om det inte direkt transformerar ett objekts nycklar *inom* Mapped Typen pĂ„ detta specifika sĂ€tt, visar det hur nycklar kan manipuleras.
Ett mer direkt exempel pÄ nyckeltransformation kan vara att mappa nycklar till deras versaliserade versioner:
type UppercaseKeys<T> = {
[P in keyof T as Uppercase<string & P>]: T[P];
};
type LowercaseData = {
firstName: string;
lastName: string;
};
type UppercaseData = UppercaseKeys<LowercaseData>;
// type UppercaseData = {
// FIRSTNAME: string;
// LASTNAME: string;
// }
Detta anvÀnder as-klausulen för att transformera varje nyckel P till dess versaliserade motsvarighet.
Praktiska tillÀmpningar och verkliga scenarier
Mapped Types Àr inte bara teoretiska konstruktioner; de har betydande praktiska implikationer inom olika utvecklingsomrÄden. HÀr Àr nÄgra vanliga scenarier dÀr de Àr ovÀrderliga:
1. Bygga ÄteranvÀndbara hjÀlptyper
MÄnga vanliga typ-transformationer kan kapslas in i ÄteranvÀndbara hjÀlptyper. Typskripts standardbibliotek tillhandahÄller redan utmÀrkta exempel som Partial<T>, Readonly<T>, Record<K, T> och Pick<T, K>. Du kan definiera dina egna anpassade hjÀlptyper med Mapped Types för att effektivisera din utvecklingsprocess.
Till exempel, en typ som mappar alla egenskaper till funktioner som accepterar det ursprungliga vÀrdet och returnerar ett nytt vÀrde:
type Mappers<T> = {
[P in keyof T]: (value: T[P]) => T[P];
};
interface ProductInfo {
name: string;
price: number;
}
type ProductMappers = Mappers<ProductInfo>;
// type ProductMappers = {
// name: (value: string) => string;
// price: (value: number) => number;
// }
2. Dynamisk formulÀrhantering och validering
Inom frontend-utveckling, sÀrskilt med ramverk som React eller Angular (Àven om exemplen hÀr Àr ren TypeScript), Àr hantering av formulÀr och deras valideringsstatus en vanlig uppgift. Mapped Types kan hjÀlpa till att hantera valideringsstatusen för varje formulÀrfÀlt.
ĂvervĂ€g ett formulĂ€r med fĂ€lt som kan vara 'pristine', 'touched', 'valid' eller 'invalid'.
type FormFieldState = 'pristine' | 'touched' | 'dirty' | 'valid' | 'invalid';
type FormState<T> = {
[P in keyof T]: FormFieldState;
};
interface UserForm {
username: string;
email: string;
password: string;
}
type UserFormState = FormState<UserForm>;
// type UserFormState = {
// username: FormFieldState;
// email: FormFieldState;
// password: FormFieldState;
// }
Detta gör att du kan skapa en typ som speglar din formulÀrs datastruktur men istÀllet spÄrar tillstÄndet för varje fÀlt, vilket sÀkerstÀller konsekvens och typsÀkerhet för din formulÀrhanteringslogik. Detta Àr sÀrskilt fördelaktigt för internationella projekt dÀr olika UI/UX-krav kan leda till komplexa formulÀrstillstÄnd.
3. API-svartransformation
Vid hantering av API:er matchar svar data inte alltid perfekt dina interna domÀnmodeller. Mapped Types kan hjÀlpa till att transformera API-svar till önskad form.
FörestÀll dig ett API-svar som anvÀnder snake_case för nycklar, men din applikation föredrar camelCase:
// Anta att detta Àr den inkommande API-svarestypen
type ApiUserData = {
user_id: number;
first_name: string;
last_name: string;
};
// HjÀlp för att konvertera snake_case till camelCase för nycklar
type ToCamelCase<S extends string>: string = S extends `${infer T}_${infer U}`
? `${T}${Capitalize<U>}`
: S;
type CamelCasedKeys<T> = {
[P in keyof T as ToCamelCase<string & P>]: T[P];
};
type AppUserData = CamelCasedKeys<ApiUserData>;
// type AppUserData = {
// userId: number;
// firstName: string;
// lastName: string;
// }
Detta Àr ett mer avancerat exempel som anvÀnder en rekursiv villkorlig typ för strÀngmanipulering. KÀrnbudskapet Àr att Mapped Types, nÀr de kombineras med andra avancerade TypeScript-funktioner, kan automatisera komplexa datatransformationer, vilket sparar utvecklingstid och minskar risken för körtidsfel. Detta Àr avgörande för globala team som arbetar med olika backend-tjÀnster.
4. FörbÀttra enum-liknande strukturer
Medan TypeScript har `enum`s, vill du ibland ha mer flexibilitet eller hÀrleda typer frÄn objektbokstav som fungerar som enums.
const AppPermissions = {
READ: 'read',
WRITE: 'write',
DELETE: 'delete',
ADMIN: 'admin',
} as const;
type Permission = typeof AppPermissions[keyof typeof AppPermissions];
// type Permission = 'read' | 'write' | 'delete' | 'admin'
type UserPermissions = {
[P in Permission]?: boolean;
};
type RolePermissions = {
[P in Permission]: boolean;
};
const userPerms: UserPermissions = {
read: true,
};
const adminRole: RolePermissions = {
read: true,
write: true,
delete: true,
admin: true,
};
HÀr hÀrleder vi först en unionstyp av alla möjliga behörighetsstrÀngar. Sedan anvÀnder vi Mapped Types för att skapa typer dÀr varje behörighet Àr en nyckel, vilket gör det möjligt för oss att specificera om en anvÀndare har den behörigheten (valfritt) eller om en roll krÀver det (obligatoriskt). Detta mönster Àr vanligt i auktoriseringssystem vÀrlden över.
Utmaningar och övervÀganden
Ăven om Mapped Types Ă€r otroligt kraftfulla, Ă€r det viktigt att vara medveten om potentiella komplexiteter:
- LÀsbarhet och komplexitet: Alltför komplexa Mapped Types kan bli svÄra att lÀsa och förstÄ, sÀrskilt för utvecklare som Àr nya inom dessa avancerade funktioner. StrÀva alltid efter tydlighet och övervÀg att lÀgga till kommentarer eller dela upp komplexa transformationer.
- Prestandainverkan: Ăven om Typskripts typkontroll sker vid kompileringstid, kan extremt komplexa typmanipuleringar, i teorin, öka kompileringstiderna nĂ„got. För de flesta applikationer Ă€r detta obetydligt, men det Ă€r en punkt att tĂ€nka pĂ„ för mycket stora kodbaser eller mycket prestandakritiska byggprocesser.
- Felsökning: NÀr en Mapped Type ger ett ovÀntat resultat kan felsökning ibland vara utmanande. Att anvÀnda TypeScript Playground eller IDE:ns typinspektionsfunktioner Àr avgörande för att förstÄ hur typer löses.
- FörstÄelse för
keyofoch uppslagstyper: Effektiv anvÀndning av Mapped Types bygger pÄ en solid förstÄelse förkeyofoch uppslagstyper (T[P]). Se till att ditt team har en god förstÄelse för dessa grundlÀggande koncept.
BÀsta metoder för anvÀndning av Mapped Types
För att utnyttja den fulla potentialen hos Mapped Types och samtidigt mildra deras utmaningar, övervÀg dessa bÀsta metoder:
- Börja enkelt: Börja med grundlÀggande valfrihet och endast lÀsbar transformationer innan du dyker ner i komplexa nyckelomkartningar eller villkorslogik.
- AnvÀnd inbyggda hjÀlptyper: Bekanta dig med Typskripts inbyggda hjÀlptyper som
Partial,Readonly,Record,Pick,OmitochExclude. De Àr ofta tillrÀckliga för vanliga uppgifter och Àr vÀltestade och förstÄdda. - Skapa ÄteranvÀndbara generiska typer: Kapsla in vanliga Mapped Type-mönster i generiska hjÀlptyper. Detta frÀmjar konsekvens och minskar kodduplicering över ditt projekt och för globala team.
- AnvÀnd beskrivande namn: Namnge dina Mapped Types och generiska parametrar tydligt för att indikera deras syfte (t.ex.
Optional<T>,DeepReadonly<T>,PrefixedKeys<T, Prefix>). - Prioritera lÀsbarhet: Om en Mapped Type blir för komplicerad, övervÀg om det finns ett enklare sÀtt att uppnÄ samma resultat eller om det Àr vÀrt den extra komplexiteten. Ibland Àr en nÄgot mer verbose men tydligare typdefinition att föredra.
- Dokumentera komplexa typer: För intrikata Mapped Types, lÀgg till JSDoc-kommentarer som förklarar deras funktionalitet, sÀrskilt nÀr du delar kod inom ett mÄngfaldigt internationellt team.
- Testa dina typer: Skriv typ-tester eller anvÀnd exempel för att verifiera att dina Mapped Types beter sig som förvÀntat. Detta Àr sÀrskilt viktigt för komplexa transformationer dÀr subtila buggar kan vara svÄra att upptÀcka.
Slutsats
TypeScript Mapped Types Àr en hörnsten i avancerad typmanipulering och erbjuder utvecklare oövertrÀffad kraft att transformera och anpassa objekttyper. Oavsett om du gör egenskaper valfria, endast lÀsbar, byter namn pÄ dem eller filtrerar dem baserat pÄ intrikata villkor, ger Mapped Types ett deklarativt, typsÀkert och mycket uttrycksfullt sÀtt att hantera dina datastrukturer.
Genom att bemÀstra dessa tekniker kan du avsevÀrt förbÀttra kodÄteranvÀndning, öka typsÀkerheten och bygga mer robusta och underhÄllbara applikationer. Omfamna kraften hos Mapped Types för att lyfta din TypeScript-utveckling och bidra till att bygga högkvalitativa mjukvarulösningar för en global publik. NÀr du samarbetar med utvecklare frÄn olika regioner kan dessa avancerade typmönster fungera som ett gemensamt sprÄk för att sÀkerstÀlla kodkvalitet och konsekvens, och överbrygga potentiella kommunikationsluckor genom typsystemets stringens.