Svenska

Utforska TypeScript branded types, en kraftfull teknik för att uppnå nominell typning i ett strukturellt typsystem. Lär dig hur du förbättrar typsäkerhet och kodtydlighet.

TypeScript Branded Types: Nominell typning i ett strukturellt system

TypeScript's strukturella typsystem erbjuder flexibilitet men kan ibland leda till oväntat beteende. Branded types är ett sätt att upprätthålla nominell typning, vilket förbättrar typsäkerheten och kodens tydlighet. Denna artikel utforskar branded types i detalj, med praktiska exempel och bästa praxis för deras implementering.

Förstå strukturell vs. nominell typning

Innan vi dyker in i branded types, låt oss klargöra skillnaden mellan strukturell och nominell typning.

Strukturell typning (Duck Typing)

I ett strukturellt typsystem anses två typer vara kompatibla om de har samma struktur (dvs. samma egenskaper med samma typer). TypeScript använder strukturell typning. Tänk på detta exempel:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valid in TypeScript

console.log(vector.x); // Output: 10

Även om Point och Vector deklareras som distinkta typer, tillåter TypeScript att ett Point-objekt tilldelas en Vector-variabel eftersom de delar samma struktur. Detta kan vara bekvämt, men det kan också leda till fel om du behöver skilja mellan logiskt olika typer som råkar ha samma form. Tänk till exempel på koordinater för latitud/longitud som av en slump kan matcha skärmpixelkoordinater.

Nominell typning

I ett nominellt typsystem anses typer vara kompatibla endast om de har samma namn. Även om två typer har samma struktur behandlas de som distinkta om de har olika namn. Språk som Java och C# använder nominell typning.

Behovet av Branded Types

TypeScript's strukturella typning kan vara problematisk när du behöver säkerställa att ett värde tillhör en specifik typ, oavsett dess struktur. Tänk till exempel på att representera valutor. Du kan ha olika typer för USD och EUR, men båda kan representeras som tal. Utan en mekanism för att skilja dem åt kan du av misstag utföra operationer på fel valuta.

Branded types löser detta problem genom att låta dig skapa distinkta typer som är strukturellt lika men behandlas som olika av typsystemet. Detta förbättrar typsäkerheten och förhindrar fel som annars skulle kunna slinka igenom.

Implementera Branded Types i TypeScript

Branded types implementeras med hjälp av intersection-typer och en unik symbol eller strängliteral. Idén är att lägga till ett "märke" (brand) till en typ som skiljer den från andra typer med samma struktur.

Använda symboler (rekommenderas)

Att använda symboler för märkning (branding) är generellt att föredra eftersom symboler garanterat är unika.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

I det här exemplet är USD och EUR branded types baserade på number-typen. Den unika symbolen säkerställer att dessa typer är distinkta. Funktionerna createUSD och createEUR används för att skapa värden av dessa typer, och funktionen addUSD accepterar endast USD-värden. Ett försök att addera ett EUR-värde till ett USD-värde kommer att resultera i ett typfel.

Använda strängliteraler

Du kan också använda strängliteraler för märkning, även om denna metod är mindre robust än att använda symboler eftersom strängliteraler inte garanterat är unika.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

Detta exempel uppnår samma resultat som det föregående, men använder strängliteraler istället för symboler. Även om det är enklare är det viktigt att säkerställa att de strängliteraler som används för märkning är unika inom din kodbas.

Praktiska exempel och användningsfall

Branded types kan tillämpas i olika scenarier där du behöver upprätthålla typsäkerhet utöver strukturell kompatibilitet.

ID:n

Tänk dig ett system med olika typer av ID:n, såsom UserID, ProductID och OrderID. Alla dessa ID:n kan representeras som tal eller strängar, men du vill förhindra oavsiktlig blandning av olika ID-typer.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... fetch user data
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... fetch product data
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// Uncommenting the next line will cause a type error
// const invalidCall = getUser(productID);

Detta exempel visar hur branded types kan förhindra att ett ProductID skickas till en funktion som förväntar sig ett UserID, vilket förbättrar typsäkerheten.

Domänspecifika värden

Branded types kan också vara användbara för att representera domänspecifika värden med begränsningar. Du kan till exempel ha en typ för procentandelar som alltid ska vara mellan 0 och 100.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Percentage must be between 0 and 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // Uncommenting the next line will cause an error during runtime
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Detta exempel visar hur man kan upprätthålla en begränsning på värdet av en branded type vid körtid. Även om typsystemet inte kan garantera att ett Percentage-värde alltid är mellan 0 och 100, kan funktionen createPercentage upprätthålla denna begränsning vid körtid. Du kan också använda bibliotek som io-ts för att upprätthålla validering av branded types vid körtid.

Representationer av datum och tid

Att arbeta med datum och tider kan vara knepigt på grund av olika format och tidszoner. Branded types kan hjälpa till att skilja mellan olika representationer av datum och tid.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Validate that the date string is in UTC format (e.g., ISO 8601 with Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Invalid UTC date format');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Validate that the date string is in local date format (e.g., YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Invalid local date format');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Perform time zone conversion
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

Detta exempel skiljer mellan UTC- och lokala datum, vilket säkerställer att du arbetar med rätt datum- och tidsrepresentation i olika delar av din applikation. Validering vid körtid säkerställer att endast korrekt formaterade datumsträngar kan tilldelas dessa typer.

Bästa praxis för att använda Branded Types

För att effektivt använda branded types i TypeScript, överväg följande bästa praxis:

Fördelar med Branded Types

Nackdelar med Branded Types

Alternativ till Branded Types

Även om branded types är en kraftfull teknik för att uppnå nominell typning i TypeScript, finns det alternativa metoder som du kan överväga.

Opaque Types

Opaque types liknar branded types men erbjuder ett mer explicit sätt att dölja den underliggande typen. TypeScript har inget inbyggt stöd för opaque types, men du kan simulera dem med hjälp av moduler och privata symboler.

Klasser

Att använda klasser kan erbjuda ett mer objektorienterat tillvägagångssätt för att definiera distinkta typer. Även om klasser är strukturellt typade i TypeScript, erbjuder de en tydligare ansvarsfördelning och kan användas för att upprätthålla begränsningar genom metoder.

Bibliotek som `io-ts` eller `zod`

Dessa bibliotek erbjuder sofistikerad typvalidering vid körtid och kan kombineras med branded types för att säkerställa säkerhet både vid kompilering och körtid.

Slutsats

TypeScript branded types är ett värdefullt verktyg för att förbättra typsäkerhet och kodtydlighet i ett strukturellt typsystem. Genom att lägga till ett "märke" (brand) till en typ kan du upprätthålla nominell typning och förhindra oavsiktlig blandning av strukturellt lika men logiskt olika typer. Även om branded types introducerar viss komplexitet och overhead, överväger fördelarna med förbättrad typsäkerhet och kodunderhållbarhet ofta nackdelarna. Överväg att använda branded types i scenarier där du behöver säkerställa att ett värde tillhör en specifik typ, oavsett dess struktur.

Genom att förstå principerna bakom strukturell och nominell typning, och genom att tillämpa bästa praxis som beskrivs i denna artikel, kan du effektivt utnyttja branded types för att skriva mer robust och underhållbar TypeScript-kod. Från att representera valutor och ID:n till att upprätthålla domänspecifika begränsningar, erbjuder branded types en flexibel och kraftfull mekanism för att förbättra typsäkerheten i dina projekt.

När du arbetar med TypeScript, utforska de olika tekniker och bibliotek som finns tillgängliga för typvalidering och upprätthållande. Överväg att använda branded types tillsammans med bibliotek för validering vid körtid som io-ts eller zod för att uppnå en heltäckande strategi för typsäkerhet.