Gitt utgangspunktet “la oss type JS” så er TypeScript ganske supert. Dette er ikke nødvendigvis et premiss vi trenger godta, men når man først besøker TS-land hjelper det å ha noen triks for hånden.
¶1. Refaktorer med @ts-expect-error
Annotasjonen @ts-ignore
ber TypeScript-kompilatoren ignorere feil på neste linje.
Den alternative annotasjonen
@ts-expect-error
gjør samme jobb på en litt bedre måte:
denne vil i tillegg rope høyt når linjen ikke lenger feiler.
// @ts-expect-error
const a: number = "1"; // ✅
// @ts-expect-error
const b: number = 1; // ❌
@ts-expect-error
er spesielt hjelpsom i kodebaser som en gang var JS,
og nå er 80% ferdig konvertert til TS (der 80% av jobben gjenstår).
For å komme helt i land kan man for eksempel:
- Endre alle gjenstående filnavn fra
.js
til.ts
, - legge til
@ts-expect-error
der det trengs, og - sette som mål å fjerne alle de nye annotasjonene.
Ved å løse én @ts-expect-error
fikses ofte flere følgefeil,
og annotasjonene forteller deg når de kan fjernes.
¶2. Spesifiser strenger med literal types
Noen ganger er det ikke nok med en streng; vi trenger også et spesifikt format. TypeScripts template literal types lar oss spesifisere formatet vi ønsker i typesystemet, slik at feil kan oppdages allerede ved kompilering:
// Denne typen beskriver en streng som må starte med "/".
type LeadingSlash = `/${string}`;
// Denne funksjonen kan kun kalles med en streng som starter med "/".
function leadingSlashPlease(path: LeadingSlash) {}
leadingSlashPlease("./a"); // ❌
leadingSlashPlease("/b"); // ✅
Hvis vi trenger en runtime-sjekk kan vi ty til en type guard:
function hasLeadingSlash(path: string): path is LeadingSlash {
return path.startsWith("/");
}
leadingSlashPlease(someString); // ❌
if (hasLeadingSlash(someString)) {
leadingSlashPlease(someString); // ✅
}
Hvis vi mot formodning trenger en exception kan vi bruke nøkkelordet asserts
:
function assertLeadingSlash(path: string): asserts path is LeadingSlash {
if (!path.startsWith("/")) {
throw new Error(`expected leading slash: "${path}"`);
}
}
leadingSlashPlease(someString); // ❌
assertLeadingSlash(someString);
leadingSlashPlease(someString); // ✅
¶3. Utforsk hjelpsomme hjelpetyper
TypeScript har mange innebygde hjelpetyper som overraskende ofte er akkurat hva som trengs:
// `Readonly` kan sikre at en funksjon ikke muterer sine argumenter.
function sortStrings(list: Readonly<string[]>): string[] {}
// `ReturnType` kan få tak i en type som ikke ble eksportert fra en avhengighet.
type MyReturnType = ReturnType<typeof someLibraryFunction>;
// `Omit` kan forenkle eksisterende typer.
type Unsaved<T> = Omit<T, "id" | "createdAt" | "updatedAt">;
Ved hjelp av conditional types kan vi også lage spennende hjelpetyper på egen hånd:
// Denne typen beskriver en streng som ikke kan være tom.
type NonEmptyString<T extends string> = T extends "" ? never : T;
// Denne funksjonen kan ikke kalles med en tom streng.
function emptyStringsAreWeird<T extends string>(s: NonEmptyString<T>) {}
emptyStringsAreWeird(""); // ❌
emptyStringsAreWeird("a"); // ✅
¶4. Synkroniser navn med Pick
Navngiving er vanskelig, så det er flott å kunne sette samme navn på samme ting overalt.
TypeScripts
Pick
kan hjelpe oss med å holde navn synkronisert ved å plukke felter fra eksisterende typer:
type ServerConfig = {
hostName: string;
};
// Hva er `name` her egentlig?
function startServer(name: string) {}
// Ah, det er `hostName` fra `ServerConfig`.
function startServer({ hostName }: Pick<ServerConfig, "hostName">) {}
¶5. Del opp globale typer
Globale domenetyper er alltid fristende, men ofte skumle saker. Store produkttyper som brukes mange steder blir fort svekket via valgfrie felter og type-unioner. Resultatet blir lett vanskelig å forstå. Når har vi hvert felt?
type User = {
id?: number;
name: string;
email?: string;
createdAt?: Date | string;
};
Et alternativ er å bruke flere, strengere typer.
Pick
og
Omit
kan brukes til å synkronisere feltenes navn og undertyper:
type UserRow = {
id: number;
name: string;
email: string;
createdAt: Date;
};
// UserRow bruker `Date`s, men JSON trenger strenger.
type UserResponse = Omit<UserRow, "createdAt"> & { createdAt: string };
// Kun `name` og `email` kan spesifiseres ved `create`.
type CreateUserRequest = Pick<UserRow, "name" | "email">;
// Oppdateringer trenger en `id`, men `email` kan ikke oppdateres.
type UpdateUserRequest = Pick<UserRow, "id" | "name">;
Globale domenetyper kan føre til sterk kobling mellom lagene i en applikasjon. Oppdelte typer tydeliggjør inndelingen og understreker forskjellene mellom hvert lag. Litt mer å skrive, men potensielt enklere å forstå.