Utforsk TypeScripts eksakte typer for streng objekttilpasning, unngå uventede egenskaper og sikre robust kode. Lær praktiske bruksområder og beste praksis.
TypeScript eksakte typer: Streng objekttilpasning for robust kode
TypeScript, et supersett av JavaScript, bringer statisk typing til den dynamiske verdenen av webutvikling. Mens TypeScript tilbyr betydelige fordeler når det gjelder typesikkerhet og vedlikehold av kode, kan dets strukturelle typingsystem noen ganger føre til uventet oppførsel. Det er her konseptet med "eksakte typer" kommer inn i bildet. Selv om TypeScript ikke har en innebygd funksjon som eksplisitt heter "eksakte typer", kan vi oppnå lignende oppførsel gjennom en kombinasjon av TypeScript-funksjoner og teknikker. Dette blogginnlegget vil dykke ned i hvordan man kan håndheve strengere objekttilpasning i TypeScript for å forbedre kodens robusthet og forhindre vanlige feil.
Forståelse av TypeScripts strukturelle typing
TypeScript bruker strukturell typing (også kjent som duck typing), noe som betyr at typekompatibilitet bestemmes av medlemmenes typer, snarere enn deres deklarerte navn. Hvis et objekt har alle egenskapene som kreves av en type, anses det som kompatibelt med den typen, uavhengig av om det har flere egenskaper.
For eksempel:
interface Point {
x: number;
y: number;
}
const myPoint = { x: 10, y: 20, z: 30 };
function printPoint(point: Point) {
console.log(`X: ${point.x}, Y: ${point.y}`);
}
printPoint(myPoint); // Dette fungerer fint, selv om myPoint har 'z'-egenskapen
I dette scenariet tillater TypeScript at `myPoint` blir sendt til `printPoint` fordi det inneholder de nødvendige `x`- og `y`-egenskapene, selv om det har en ekstra `z`-egenskap. Selv om denne fleksibiliteten kan være praktisk, kan den også føre til subtile feil hvis du utilsiktet sender objekter med uventede egenskaper.
Problemet med overskytende egenskaper
Fleksibiliteten i strukturell typing kan noen ganger skjule feil. Vurder en funksjon som forventer et konfigurasjonsobjekt:
interface Config {
apiUrl: string;
timeout: number;
}
function setup(config: Config) {
console.log(`API URL: ${config.apiUrl}`);
console.log(`Timeout: ${config.timeout}`);
}
const myConfig = { apiUrl: "https://api.example.com", timeout: 5000, typo: true };
setup(myConfig); // TypeScript klager ikke her!
console.log(myConfig.typo); //skriver ut true. Den ekstra egenskapen eksisterer i stillhet
I dette eksempelet har `myConfig` en ekstra egenskap `typo`. TypeScript gir ingen feilmelding fordi `myConfig` fortsatt tilfredsstiller `Config`-grensesnittet. Skrivefeilen blir imidlertid aldri fanget opp, og applikasjonen oppfører seg kanskje ikke som forventet hvis skrivefeilen var ment å være `typoo`. Disse tilsynelatende ubetydelige problemene kan vokse til store hodepiner ved feilsøking av komplekse applikasjoner. En manglende eller feilstavet egenskap kan være spesielt vanskelig å oppdage når man arbeider med nøstede objekter.
Tilnærminger for å håndheve eksakte typer i TypeScript
Selv om ekte "eksakte typer" ikke er direkte tilgjengelige i TypeScript, er det flere teknikker for å oppnå lignende resultater og håndheve strengere objekttilpasning:
1. Bruke type-påstander med Omit
Verktøytypen `Omit` lar deg lage en ny type ved å ekskludere visse egenskaper fra en eksisterende type. Kombinert med en type-påstand kan dette bidra til å forhindre overskytende egenskaper.
interface Point {
x: number;
y: number;
}
const myPoint = { x: 10, y: 20, z: 30 };
// Lag en type som kun inkluderer egenskapene til Point
const exactPoint: Point = myPoint as Omit & Point;
// Feil: Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'.
// Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
function printPoint(point: Point) {
console.log(`X: ${point.x}, Y: ${point.y}`);
}
//Rettelse
const myPointCorrect = { x: 10, y: 20 };
const exactPointCorrect: Point = myPointCorrect as Omit & Point;
printPoint(exactPointCorrect);
Denne tilnærmingen gir en feilmelding hvis `myPoint` har egenskaper som ikke er definert i `Point`-grensesnittet.
Forklaring: `Omit
2. Bruke en funksjon til å lage objekter
Du kan lage en fabrikkfunksjon som bare godtar egenskapene definert i grensesnittet. Denne tilnærmingen gir sterk typekontroll på tidspunktet for opprettelse av objektet.
interface Config {
apiUrl: string;
timeout: number;
}
function createConfig(config: Config): Config {
return {
apiUrl: config.apiUrl,
timeout: config.timeout,
};
}
const myConfig = createConfig({ apiUrl: "https://api.example.com", timeout: 5000 });
//Dette vil ikke kompilere:
//const myConfigError = createConfig({ apiUrl: "https://api.example.com", timeout: 5000, typo: true });
//Argument of type '{ apiUrl: string; timeout: number; typo: true; }' is not assignable to parameter of type 'Config'.
// Object literal may only specify known properties, and 'typo' does not exist in type 'Config'.
Ved å returnere et objekt konstruert med kun egenskapene definert i `Config`-grensesnittet, sikrer du at ingen ekstra egenskaper kan snike seg inn. Dette gjør det tryggere å opprette konfigurasjonen.
3. Bruke Type Guards
Type guards er funksjoner som begrenser typen til en variabel innenfor et spesifikt omfang. Selv om de ikke direkte forhindrer overskytende egenskaper, kan de hjelpe deg med å eksplisitt sjekke for dem og iverksette passende tiltak.
interface User {
id: number;
name: string;
}
function isUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj && typeof obj.id === 'number' &&
'name' in obj && typeof obj.name === 'string' &&
Object.keys(obj).length === 2 //sjekk antall nøkler. Merk: skjørt og avhenger av det nøyaktige antallet nøkler i User.
);
}
const potentialUser1 = { id: 123, name: "Alice" };
const potentialUser2 = { id: 456, name: "Bob", extra: true };
if (isUser(potentialUser1)) {
console.log("Valid User:", potentialUser1.name);
} else {
console.log("Invalid User");
}
if (isUser(potentialUser2)) {
console.log("Valid User:", potentialUser2.name); //Vil ikke treffe her
} else {
console.log("Invalid User");
}
I dette eksempelet sjekker `isUser`-typevakten ikke bare for tilstedeværelsen av nødvendige egenskaper, men også for deres typer og det *nøyaktige* antallet egenskaper. Denne tilnærmingen er mer eksplisitt og lar deg håndtere ugyldige objekter på en ryddig måte. Imidlertid er sjekken for antall egenskaper skjør. Hver gang `User` får/mister egenskaper, må sjekken oppdateres.
4. Utnytte `Readonly` og `as const`
Mens `Readonly` forhindrer modifisering av eksisterende egenskaper, og `as const` lager en skrivebeskyttet tuppel eller objekt hvor alle egenskaper er dypt skrivebeskyttede og har litterale typer, kan de brukes til å lage en strengere definisjon og typekontroll når de kombineres med andre metoder. Ingen av dem forhindrer imidlertid overskytende egenskaper på egen hånd.
interface Options {
width: number;
height: number;
}
//Lag Readonly-typen
type ReadonlyOptions = Readonly;
const options: ReadonlyOptions = { width: 100, height: 200 };
//options.width = 300; //feil: Cannot assign to 'width' because it is a read-only property.
//Bruker as const
const config = { api_url: "https://example.com", timeout: 3000 } as const;
//config.timeout = 5000; //feil: Cannot assign to 'timeout' because it is a read-only property.
//Imidlertid er overskytende egenskaper fortsatt tillatt:
const invalidOptions: ReadonlyOptions = { width: 100, height: 200, depth: 300 }; //ingen feil. Tillater fortsatt overskytende egenskaper.
interface StrictOptions {
readonly width: number;
readonly height: number;
}
//Dette vil nå gi en feil:
//const invalidStrictOptions: StrictOptions = { width: 100, height: 200, depth: 300 };
//Type '{ width: number; height: number; depth: number; }' is not assignable to type 'StrictOptions'.
// Object literal may only specify known properties, and 'depth' does not exist in type 'StrictOptions'.
Dette forbedrer immutabilitet, men forhindrer bare mutasjon, ikke eksistensen av ekstra egenskaper. Kombinert med `Omit`, eller funksjonstilnærmingen, blir det mer effektivt.
5. Bruke biblioteker (f.eks. Zod, io-ts)
Biblioteker som Zod og io-ts tilbyr kraftig kjøretidsvalidering av typer og skjemadefinisjonsmuligheter. Disse bibliotekene lar deg definere skjemaer som nøyaktig beskriver den forventede formen på dataene dine, inkludert å forhindre overskytende egenskaper. Selv om de legger til en kjøretidsavhengighet, tilbyr de en veldig robust og fleksibel løsning.
Eksempel med Zod:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer;
const validUser = { id: 1, name: "John" };
const invalidUser = { id: 2, name: "Jane", extra: true };
const parsedValidUser = UserSchema.parse(validUser);
console.log("Parsed Valid User:", parsedValidUser);
try {
const parsedInvalidUser = UserSchema.parse(invalidUser);
console.log("Parsed Invalid User:", parsedInvalidUser); // Dette vil ikke bli nådd
} catch (error) {
console.error("Validation Error:", error.errors);
}
Zods `parse`-metode vil kaste en feil hvis inputen ikke samsvarer med skjemaet, og effektivt forhindre overskytende egenskaper. Dette gir kjøretidsvalidering og genererer også TypeScript-typer fra skjemaet, noe som sikrer konsistens mellom typedefinisjonene dine og kjøretidsvalideringslogikken.
Beste praksis for å håndheve eksakte typer
Her er noen beste praksiser å vurdere når du håndhever strengere objekttilpasning i TypeScript:
- Velg riktig teknikk: Den beste tilnærmingen avhenger av dine spesifikke behov og prosjektkrav. For enkle tilfeller kan type-påstander med `Omit` eller fabrikkfunksjoner være tilstrekkelig. For mer komplekse scenarier eller når kjøretidsvalidering er nødvendig, bør du vurdere å bruke biblioteker som Zod eller io-ts.
- Vær konsekvent: Bruk din valgte tilnærming konsekvent gjennom hele kodebasen for å opprettholde et jevnt nivå av typesikkerhet.
- Dokumenter typene dine: Dokumenter grensesnittene og typene dine tydelig for å kommunisere den forventede formen på dataene dine til andre utviklere.
- Test koden din: Skriv enhetstester for å verifisere at typebegrensningene dine fungerer som forventet, og at koden din håndterer ugyldige data på en ryddig måte.
- Vurder avveiningene: Å håndheve strengere objekttilpasning kan gjøre koden din mer robust, men det kan også øke utviklingstiden. Vei fordelene mot kostnadene og velg den tilnærmingen som gir mest mening for prosjektet ditt.
- Gradvis adopsjon: Hvis du jobber med en stor eksisterende kodebase, bør du vurdere å ta i bruk disse teknikkene gradvis, og starte med de mest kritiske delene av applikasjonen din.
- Foretrekk grensesnitt fremfor type-aliaser når du definerer objektformer: Grensesnitt er generelt foretrukket fordi de støtter deklarasjonssammenslåing, noe som kan være nyttig for å utvide typer på tvers av forskjellige filer.
Eksempler fra den virkelige verden
La oss se på noen virkelige scenarier der eksakte typer kan være fordelaktige:
- API-forespørselslaster (payloads): Når du sender data til et API, er det avgjørende å sikre at lasten samsvarer med det forventede skjemaet. Håndheving av eksakte typer kan forhindre feil forårsaket av sending av uventede egenskaper. For eksempel er mange betalingsbehandlings-API-er ekstremt følsomme for uventede data.
- Konfigurasjonsfiler: Konfigurasjonsfiler inneholder ofte et stort antall egenskaper, og skrivefeil kan være vanlig. Bruk av eksakte typer kan hjelpe til med å fange disse skrivefeilene tidlig. Hvis du setter opp serverplasseringer i en sky-distribusjon, vil en skrivefeil i en lokasjonsinnstilling (f.eks. eu-west-1 vs. eu-wet-1) bli ekstremt vanskelig å feilsøke hvis den ikke blir fanget opp med en gang.
- Datatransformasjons-pipelines: Når du transformerer data fra ett format til et annet, er det viktig å sikre at utdataene samsvarer med det forventede skjemaet.
- Meldingskøer: Når du sender meldinger gjennom en meldingskø, er det viktig å sikre at meldingens nyttelast er gyldig og inneholder de riktige egenskapene.
Eksempel: Konfigurasjon for internasjonalisering (i18n)
Tenk deg at du administrerer oversettelser for en flerspråklig applikasjon. Du kan ha et konfigurasjonsobjekt som dette:
interface Translation {
greeting: string;
farewell: string;
}
interface I18nConfig {
locale: string;
translations: Translation;
}
const englishConfig: I18nConfig = {
locale: "en-US",
translations: {
greeting: "Hello",
farewell: "Goodbye"
}
};
//Dette vil være et problem, siden en overskytende egenskap eksisterer, noe som stille introduserer en feil.
const spanishConfig: I18nConfig = {
locale: "es-ES",
translations: {
greeting: "Hola",
farewell: "Adiós",
typo: "unintentional translation"
}
};
//Løsning: Bruke Omit
const spanishConfigCorrect: I18nConfig = {
locale: "es-ES",
translations: {
greeting: "Hola",
farewell: "Adiós"
} as Omit & Translation
};
Uten eksakte typer kan en skrivefeil i en oversettelsesnøkkel (som å legge til et `typo`-felt) gå ubemerket hen, noe som fører til manglende oversettelser i brukergrensesnittet. Ved å håndheve strengere objekttilpasning kan du fange disse feilene under utvikling og forhindre at de når produksjon.
Konklusjon
Selv om TypeScript ikke har innebygde "eksakte typer", kan du oppnå lignende resultater ved å bruke en kombinasjon av TypeScript-funksjoner og teknikker som type-påstander med `Omit`, fabrikkfunksjoner, type guards, `Readonly`, `as const`, og eksterne biblioteker som Zod og io-ts. Ved å håndheve strengere objekttilpasning kan du forbedre robustheten til koden din, forhindre vanlige feil og gjøre applikasjonene dine mer pålitelige. Husk å velge den tilnærmingen som passer best for dine behov og å være konsekvent i bruken av den gjennom hele kodebasen. Ved å nøye vurdere disse tilnærmingene kan du ta større kontroll over applikasjonens typer og øke langsiktig vedlikeholdbarhet.