Opi käyttämään TypeScriptin Mapped Types -tyyppejä dynaamisiin objektimuunnoksiin, parantaen koodin uudelleenkäytettävyyttä ja tyyppiturvallisuutta globaaleille kehittäjille.
TypeScriptin Mapped Types: Objektien muunnoksen ja ominaisuuksien muokkauksen hallinta
Jatkuvasti kehittyvässä ohjelmistokehityksen maailmassa vankat tyyppijärjestelmät ovat ensisijaisen tärkeitä ylläpidettävien, skaalautuvien ja luotettavien sovellusten rakentamisessa. TypeScript, voimakkaalla tyyppipäättelyllään ja edistyneillä ominaisuuksillaan, on muodostunut korvaamattomaksi työkaluksi kehittäjille maailmanlaajuisesti. Sen tehokkaimpiin ominaisuuksiin kuuluvat Mapped Types, hienostunut mekanismi, joka mahdollistaa olemassa olevien objektityyppien muuntamisen uusiksi. Tämä blogikirjoitus sukeltaa syvälle TypeScriptin Mapped Types -maailmaan, tutkien niiden peruskäsitteitä, käytännön sovelluksia ja sitä, kuinka ne antavat kehittäjille mahdollisuuden käsitellä elegantisti objektien muunnoksia ja ominaisuuksien muokkauksia.
Mapped Types -tyyppien peruskäsitteen ymmärtäminen
Pohjimmiltaan Mapped Type on tapa luoda uusia tyyppejä iteroimalla olemassa olevan tyypin ominaisuuksien yli. Ajattele sitä silmukana tyypeille. Jokaiselle alkuperäisen tyypin ominaisuudelle voit soveltaa muunnoksen sen avaimeen, arvoon tai molempiin. Tämä avaa laajan valikoiman mahdollisuuksia uusien tyyppimääritysten luomiseen olemassa olevien pohjalta, ilman manuaalista toistoa.
Mapped Type -tyypin perussyntaksi sisältää rakenteen { [P in K]: T }, jossa:
P: Edustaa iteroitavan ominaisuuden nimeä.in K: Tämä on olennainen osa, joka ilmaisee, ettäPsaa arvokseen jokaisen avaimen tyypistäK(joka on tyypillisesti merkkijonoliteraalien unioni tai keyof-tyyppi).T: Määrittelee ominaisuudenParvon tyypin uudessa tyypissä.
Aloitetaan yksinkertaisella esimerkillä. Kuvittele, että sinulla on objekti, joka edustaa käyttäjätietoja, ja haluat luoda uuden tyypin, jossa kaikki ominaisuudet ovat valinnaisia. Tämä on yleinen tilanne esimerkiksi konfiguraatio-objekteja rakennettaessa tai osittaisia päivityksiä toteutettaessa.
Esimerkki 1: Kaikkien ominaisuuksien tekeminen valinnaisiksi
Tarkastellaan tätä perustyyppiä:
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
};
Voimme luoda uuden tyypin, OptionalUser, jossa kaikki nämä ominaisuudet ovat valinnaisia käyttämällä Mapped Type -tyyppiä:
type OptionalUser = {
[P in keyof User]?: User[P];
};
Puretaan tämä osiin:
keyof User: Tämä luo unioninUser-tyypin avaimista (esim.'id' | 'name' | 'email' | 'isActive').P in keyof User: Tämä iteroi jokaisen avaimen yli unionissa.?: Tämä on muunnin, joka tekee ominaisuudesta valinnaisen.User[P]: Tämä on hakutyyppi (lookup type). Jokaiselle avaimellePse hakee vastaavan arvon tyypin alkuperäisestäUser-tyypistä.
Tuloksena oleva OptionalUser-tyyppi näyttäisi tältä:
{
id?: number;
name?: string;
email?: string;
isActive?: boolean;
}
Tämä on uskomattoman tehokasta. Sen sijaan, että määrittelisimme jokaisen ominaisuuden manuaalisesti uudelleen ?-merkillä, olemme luoneet tyypin dynaamisesti. Tätä periaatetta voidaan laajentaa monien muiden aputyyppien luomiseen.
Yleiset ominaisuusmuuntimet Mapped Types -tyypeissä
Mapped Types -tyypeissä ei ole kyse vain ominaisuuksien tekemisestä valinnaisiksi. Ne mahdollistavat erilaisten muuntimien soveltamisen tuloksena syntyvän tyypin ominaisuuksiin. Yleisimpiä ovat:
- Valinnaisuus:
?-muuntimen lisääminen tai poistaminen. - Vain luku (Readonly):
readonly-muuntimen lisääminen tai poistaminen. - Nollattavuus/Ei-nollattavuus:
| nulltai| undefinedlisääminen tai poistaminen.
Esimerkki 2: Vain luku -version luominen tyypistä
Samoin kuin ominaisuuksien tekeminen valinnaisiksi, voimme luoda ReadonlyUser-tyypin:
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
Tämä tuottaa:
{
readonly id: number;
readonly name: string;
readonly email: string;
readonly isActive: boolean;
}
Tämä on erittäin hyödyllistä varmistettaessa, että tiettyjä tietorakenteita ei voi muuttaa niiden luomisen jälkeen. Tämä on perusperiaate vankkojen, ennustettavien järjestelmien rakentamisessa, erityisesti rinnakkaisissa ympäristöissä tai käsiteltäessä muuttumattomia datamalleja, jotka ovat suosittuja monien kansainvälisten kehitystiimien omaksumissa funktionaalisissa ohjelmointiparadigmoissa.
Esimerkki 3: Valinnaisuuden ja vain luku -ominaisuuden yhdistäminen
Voimme yhdistää muuntimia. Esimerkiksi tyyppi, jossa ominaisuudet ovat sekä valinnaisia että vain luku -muotoisia:
type OptionalReadonlyUser = {
readonly [P in keyof User]?: User[P];
};
Tämä johtaa:
{
readonly id?: number;
readonly name?: string;
readonly email?: string;
readonly isActive?: boolean;
}
Muuntimien poistaminen Mapped Types -tyypeillä
Mitä jos haluat poistaa muuntimen? TypeScript mahdollistaa tämän käyttämällä -? ja -readonly -syntaksia Mapped Types -tyypeissä. Tämä on erityisen tehokasta käsiteltäessä olemassa olevia aputyyppejä tai monimutkaisia tyyppiyhdistelmiä.
Oletetaan, että sinulla on Partial<T>-tyyppi (joka on sisäänrakennettu ja tekee kaikista ominaisuuksista valinnaisia), ja haluat luoda tyypin, joka on sama kuin Partial<T>, mutta jossa kaikki ominaisuudet on tehty jälleen pakollisiksi.
type Mandatory<T> = {
-?: T extends object ? T[keyof T] : never;
};
type FullyPopulatedUser = Mandatory<Partial<User>>;
Tämä tuntuu epäintuitiiviselta. Analysoidaan sitä:
Partial<User> vastaa meidän OptionalUser-tyyppiämme. Nyt haluamme tehdä sen ominaisuuksista pakollisia. Syntaksi -? poistaa valinnaisuusmuuntimen.
Suorempi tapa saavuttaa tämä, ilman että turvaudutaan ensin Partial-tyyppiin, on yksinkertaisesti ottaa alkuperäinen tyyppi ja tehdä siitä pakollinen, jos se oli valinnainen:
type MakeMandatory<T> = {
-?: T;
};
type MandatoryUser = MakeMandatory<OptionalUser>;
Tämä palauttaa OptionalUser-tyypin oikein takaisin alkuperäiseen User-tyypin rakenteeseen (kaikki ominaisuudet ovat läsnä ja vaadittuja).
Vastaavasti, poistaaksesi readonly-muuntimen:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type MutableUser = Mutable<ReadonlyUser>;
MutableUser vastaa alkuperäistä User-tyyppiä, mutta sen ominaisuudet eivät ole vain luku -muotoisia.
Nollattavuus ja määrittelemättömyys
Voit myös hallita nollattavuutta. Esimerkiksi varmistaaksesi, että kaikki ominaisuudet ovat ehdottomasti ei-nollattavia:
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;
// }
Tässä -? varmistaa, että ominaisuudet eivät ole valinnaisia, ja NonNullable<T[P]> poistaa null ja undefined arvon tyypistä.
Ominaisuusavainten muuntaminen
Mapped Types ovat uskomattoman monipuolisia, eivätkä ne rajoitu vain arvojen tai muuntimien muokkaamiseen. Voit myös muuntaa objektityypin avaimia. Tässä Mapped Types todella loistavat monimutkaisissa skenaarioissa.
Esimerkki 4: Ominaisuusavainten etuliitteen lisääminen
Oletetaan, että haluat luoda uuden tyypin, jossa kaikilla olemassa olevan tyypin ominaisuuksilla on tietty etuliite. Tämä voi olla hyödyllistä nimiavaruuksien luomisessa tai tietorakenteiden variaatioiden generoimisessa.
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;
// }
Puretaan avaimen muunnos osiin:
P in keyof T: Iteroi edelleen alkuperäisten avainten yli.as `${Prefix}${Capitalize<string & P>}`: Tämä on avaimen uudelleenkartoituslauseke.`${Prefix}${...}`: Tämä käyttää template literal -tyyppejä uuden avaimen nimen rakentamiseen yhdistämällä annetunPrefix-etuliitteen muunnettuun ominaisuuden nimeen.Capitalize<string & P>: Tämä on yleinen tapa varmistaa, että ominaisuuden nimeäPkäsitellään merkkijonona ja sitten muutetaan sen ensimmäinen kirjain isoksi. Käytämmestring & P-leikkausta varmistaaksemme, että TypeScript käsitteleeP:tä merkkijonotyyppinä, mikä on välttämätöntäCapitalize-tyypille.
Tämä esimerkki osoittaa, kuinka voit dynaamisesti nimetä ominaisuuksia uudelleen olemassa olevien perusteella. Tämä on tehokas tekniikka yhtenäisyyden ylläpitämiseksi sovelluksen eri kerroksissa tai integroiduttaessa ulkoisiin järjestelmiin, joilla on omat nimeämiskäytäntönsä.
Esimerkki 5: Ominaisuuksien suodattaminen
Mitä jos haluat sisällyttää vain ominaisuuksia, jotka täyttävät tietyn ehdon? Tämä voidaan saavuttaa yhdistämällä Mapped Types -tyypit ehdollisiin tyyppeihin (Conditional Types) ja as-lausekkeeseen avainten uudelleenkartoitusta varten, usein ominaisuuksien suodattamiseksi.
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;
// }
Tässä tapauksessa:
T[P] extends string ? P : never: Jokaiselle ominaisuudellePtarkistamme, onko sen arvotyyppi (T[P]) yhteensopivastring-tyypin kanssa.- Jos se on merkkijono, avain
Psäilytetään. - Jos se ei ole merkkijono, se kartoitetaan
never-tyyppiin. Kun avain kartoitetaannever-tyyppiin, se poistetaan tehokkaasti tuloksena olevasta objektityypistä.
Tämä tekniikka on korvaamaton luotaessa tarkempia tyyppejä laajemmista, esimerkiksi poimimalla vain tietyntyyppisiä konfiguraatioasetuksia tai erottelemalla datakenttiä niiden luonteen mukaan.
Esimerkki 6: Avainten muuntaminen eri muotoon
Voit myös muuntaa avaimia täysin erilaisiksi avaimiksi, esimerkiksi muuttamalla merkkijonoavaimia numeroiksi tai päinvastoin, vaikka tämä on harvinaisempaa suorassa objektimanipulaatiossa ja enemmän edistyneessä tyyppitason ohjelmoinnissa.
Harkitse merkkijonoavainten muuttamista merkkijonoliteraalien unioniksi ja sen käyttämistä uuden tyypin perustana. Vaikka tämä ei suoraan muunna objektin avaimia *sisällä* Mapped Type -tyypissä tällä nimenomaisella tavalla, se osoittaa, kuinka avaimia voidaan manipuloida.
Suorempi esimerkki avainten muuntamisesta voisi olla avainten kartoittaminen niiden isokirjaimisiksi versioiksi:
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;
// }
Tämä käyttää as-lauseketta muuntaakseen jokaisen avaimen P sen isokirjaimiseksi vastineeksi.
Käytännön sovellukset ja tosielämän skenaariot
Mapped Types eivät ole vain teoreettisia rakenteita; niillä on merkittäviä käytännön vaikutuksia eri kehitysalueilla. Tässä on muutama yleinen skenaario, joissa ne ovat korvaamattomia:
1. Uudelleenkäytettävien aputyyppien rakentaminen
Monet yleiset tyyppimuunnokset voidaan kapseloida uudelleenkäytettäviin aputyyppeihin. TypeScriptin standardikirjasto tarjoaa jo erinomaisia esimerkkejä, kuten Partial<T>, Readonly<T>, Record<K, T> ja Pick<T, K>. Voit määrittää omia mukautettuja aputyyppejäsi Mapped Types -tyyppien avulla tehostaaksesi kehitystyönkulkuasi.
Esimerkiksi tyyppi, joka kartoittaa kaikki ominaisuudet funktioiksi, jotka hyväksyvät alkuperäisen arvon ja palauttavat uuden arvon:
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. Dynaaminen lomakkeiden käsittely ja validointi
Frontend-kehityksessä, erityisesti Reactin tai Angularin kaltaisten kehysten kanssa (vaikka esimerkit tässä ovat puhdasta TypeScriptiä), lomakkeiden ja niiden validointitilojen käsittely on yleinen tehtävä. Mapped Types voivat auttaa hallitsemaan kunkin lomakekentän validointitilaa.
Harkitse lomaketta, jonka kentät voivat olla 'koskematon', 'kosketettu', 'validi' tai 'epävalidi'.
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;
// }
Tämä antaa sinulle mahdollisuuden luoda tyypin, joka peilaa lomakkeesi tietorakennetta, mutta sen sijaan seuraa kunkin kentän tilaa, varmistaen johdonmukaisuuden ja tyyppiturvallisuuden lomakkeenhallintalogiikallesi. Tämä on erityisen hyödyllistä kansainvälisissä projekteissa, joissa monipuoliset UI/UX-vaatimukset voivat johtaa monimutkaisiin lomaketiloihin.
3. API-vastausten muuntaminen
API-rajapintojen kanssa työskennellessä vastausdata ei aina välttämättä vastaa täydellisesti sisäisiä verkkotunnusmallejasi. Mapped Types voivat auttaa muuntamaan API-vastauksia haluttuun muotoon.
Kuvittele API-vastaus, joka käyttää snake_case-muotoa avaimille, mutta sovelluksesi suosii camelCase-muotoa:
// Oletetaan, että tämä on saapuvan API-vastauksen tyyppi
type ApiUserData = {
user_id: number;
first_name: string;
last_name: string;
};
// Aputyyppi snake_case-muodon muuntamiseksi camelCase-muotoon avaimille
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;
// }
Tämä on edistyneempi esimerkki, jossa käytetään rekursiivista ehdollista tyyppiä merkkijonojen manipulointiin. Tärkein opetus on, että Mapped Types, yhdistettynä muihin edistyneisiin TypeScript-ominaisuuksiin, voivat automatisoida monimutkaisia datamuunnoksia, säästäen kehitysaikaa ja vähentäen ajonaikaisten virheiden riskiä. Tämä on ratkaisevan tärkeää globaaleille tiimeille, jotka työskentelevät erilaisten backend-palveluiden kanssa.
4. Enum-kaltaisten rakenteiden parantaminen
Vaikka TypeScriptissä on enum-tyyppejä, joskus saatat haluta enemmän joustavuutta tai johtaa tyyppejä objektiliteraaleista, jotka toimivat enumeiden tavoin.
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,
};
Tässä johdamme ensin unionityypin kaikista mahdollisista käyttöoikeusmerkkijonoista. Sitten käytämme Mapped Types -tyyppejä luodaksemme tyyppejä, joissa kukin käyttöoikeus on avain, mikä antaa meille mahdollisuuden määrittää, onko käyttäjällä kyseinen oikeus (valinnainen) vai edellyttääkö rooli sitä (pakollinen). Tämä malli on yleinen valtuutusjärjestelmissä maailmanlaajuisesti.
Haasteet ja huomioon otettavat seikat
Vaikka Mapped Types ovat uskomattoman tehokkaita, on tärkeää olla tietoinen mahdollisista monimutkaisuuksista:
- Luettavuus ja monimutkaisuus: Liian monimutkaiset Mapped Types -tyypit voivat muuttua vaikealukuisiksi ja -ymmärteisiksi, erityisesti kehittäjille, jotka ovat uusia näiden edistyneiden ominaisuuksien parissa. Pyri aina selkeyteen ja harkitse kommenttien lisäämistä tai monimutkaisten muunnosten pilkkomista.
- Suorituskykyvaikutukset: Vaikka TypeScriptin tyyppitarkistus on käännösaikaista, erittäin monimutkaiset tyyppimanipulaatiot voivat teoriassa hieman pidentää käännösaikoja. Useimmissa sovelluksissa tämä on merkityksetöntä, mutta se on hyvä pitää mielessä erittäin suurissa koodikannoissa tai suorituskykykriittisissä käännösprosesseissa.
- Virheenkorjaus: Kun Mapped Type tuottaa odottamattoman tuloksen, virheenkorjaus voi joskus olla haastavaa. TypeScript Playgroundin tai IDE:n tyyppitarkastustyökalujen käyttö on ratkaisevan tärkeää ymmärtääksesi, miten tyypit ratkaistaan.
keyof- ja hakutyyppien ymmärtäminen: Mapped Types -tyyppien tehokas käyttö perustuu vankkaan ymmärrykseenkeyof- ja hakutyypeistä (T[P]). Varmista, että tiimilläsi on hyvä käsitys näistä peruskäsitteistä.
Parhaat käytännöt Mapped Types -tyyppien käyttöön
Hyödyntääksesi Mapped Types -tyyppien täyden potentiaalin ja lieventääksesi niiden haasteita, harkitse näitä parhaita käytäntöjä:
- Aloita yksinkertaisesta: Aloita perusmuunnoksista, kuten valinnaisuudesta ja vain luku -ominaisuudesta, ennen kuin sukellat monimutkaisiin avainten uudelleenkartoituksiin tai ehdolliseen logiikkaan.
- Hyödynnä sisäänrakennettuja aputyyppejä: Tutustu TypeScriptin sisäänrakennettuihin aputyyppeihin, kuten
Partial,Readonly,Record,Pick,OmitjaExclude. Ne ovat usein riittäviä yleisiin tehtäviin ja ovat hyvin testattuja ja ymmärrettyjä. - Luo uudelleenkäytettäviä geneerisiä tyyppejä: Kapseloi yleiset Mapped Type -mallit geneerisiin aputyyppeihin. Tämä edistää johdonmukaisuutta ja vähentää toistuvaa koodia projektissasi ja globaaleille tiimeille.
- Käytä kuvaavia nimiä: Nimeä Mapped Types -tyyppisi ja geneeriset parametrisi selkeästi osoittamaan niiden tarkoitusta (esim.
Optional<T>,DeepReadonly<T>,PrefixedKeys<T, Prefix>). - Priorisoi luettavuutta: Jos Mapped Type muuttuu liian monimutkaiseksi, harkitse, onko olemassa yksinkertaisempi tapa saavuttaa sama tulos tai onko se lisätyn monimutkaisuuden arvoista. Joskus hieman laajempi mutta selkeämpi tyyppimääritys on parempi.
- Dokumentoi monimutkaiset tyypit: Lisää JSDoc-kommentteja monimutkaisiin Mapped Types -tyyppeihin selittämään niiden toiminnallisuutta, erityisesti jaettaessa koodia monimuotoisessa kansainvälisessä tiimissä.
- Testaa tyyppisi: Kirjoita tyyppitestejä tai käytä esimerkkejä varmistaaksesi, että Mapped Types -tyyppisi toimivat odotetusti. Tämä on erityisen tärkeää monimutkaisissa muunnoksissa, joissa hienovaraisia virheitä voi olla vaikea havaita.
Johtopäätös
TypeScriptin Mapped Types ovat edistyneen tyyppimanipulaation kulmakivi, tarjoten kehittäjille vertaansa vailla olevan voiman muuntaa ja mukauttaa objektityyppejä. Olitpa tekemässä ominaisuuksista valinnaisia, vain luku -muotoisia, nimeämässä niitä uudelleen tai suodattamassa niitä monimutkaisten ehtojen perusteella, Mapped Types tarjoavat deklaratiivisen, tyyppiturvallisen ja erittäin ilmaisuvoimaisen tavan hallita tietorakenteitasi.
Hallitsemalla nämä tekniikat voit merkittävästi parantaa koodin uudelleenkäytettävyyttä, lisätä tyyppiturvallisuutta ja rakentaa vankempia ja ylläpidettävämpiä sovelluksia. Hyödynnä Mapped Types -tyyppien voima nostaaksesi TypeScript-kehityksesi uudelle tasolle ja osallistuaksesi laadukkaiden ohjelmistoratkaisujen rakentamiseen globaalille yleisölle. Kun teet yhteistyötä eri alueilta tulevien kehittäjien kanssa, nämä edistyneet tyyppimallit voivat toimia yhteisenä kielenä koodin laadun ja johdonmukaisuuden varmistamisessa, kuroen umpeen mahdollisia viestintäkuiluja tyyppijärjestelmän kurinalaisuuden kautta.