Eesti

Avastage TypeScript'i variatsiooni annotatsioonide ja tüübiparameetrite piirangute võimsus, et luua paindlikumat, turvalisemat ja hooldatavamat koodi. Sügavuti minev ülevaade praktiliste näidetega.

TypeScript'i variatsiooni annotatsioonid: tüübiparameetrite piirangute meisterlik kasutamine robustse koodi loomiseks

TypeScript, mis on JavaScripti superkomplekt, pakub staatilist tüüpimist, parandades koodi usaldusväärsust ja hooldatavust. Üks TypeScripti täpsemaid, kuid võimsamaid funktsioone on selle tugi variatsiooni annotatsioonidele koos tüübiparameetrite piirangutega. Nende kontseptsioonide mõistmine on tõeliselt robustse ja paindliku geneerilise koodi kirjutamisel ülioluline. See blogipostitus süveneb variatsiooni, kovariantsuse, kontravariantsuse ja invariantsuse teemadesse, selgitades, kuidas tüübiparameetrite piiranguid tõhusalt kasutada turvalisemate ja korduvkasutatavamate komponentide loomiseks.

Variatsiooni mõistmine

Variatsioon kirjeldab, kuidas tüüpidevaheline alamtüübi seos mõjutab konstrueeritud tüüpide (nt geneeriliste tüüpide) vahelist alamtüübi seost. Vaatame põhitermineid lähemalt:

Kõige lihtsam on seda meeles pidada analoogia abil: kujutage ette tehast, mis toodab koerte kaelarihmu. Kovariantne tehas suudaks toota kaelarihmu igat tüüpi loomadele, kui ta suudab toota kaelarihmu koertele, säilitades alamtüüpide suhte. Kontravariantne tehas on selline, mis suudab *tarbida* igat tüüpi loomade kaelarihmu, eeldusel, et ta suudab tarbida koerte kaelarihmu. Kui tehas suudab töötada ainult koerte kaelarihmadega ja mitte millegi muuga, on see looma tüübi suhtes invariantne.

Miks on variatsioon oluline?

Variatsiooni mõistmine on tüübiohutu koodi kirjutamisel ülioluline, eriti geneerikute puhul. Kovariantsuse või kontravariantsuse vale eeldamine võib viia käitusaja vigadeni, mida TypeScripti tüübisüsteem on loodud ennetama. Vaatleme seda vigast näidet (JavaScriptis, kuid illustreerib kontseptsiooni):

// JavaScripti näide (ainult illustreeriv, MITTE TypeScript)
function modifyAnimals(animals, modifier) {
  for (let i = 0; i < animals.length; i++) {
    animals[i] = modifier(animals[i]);
  }
}

function sound(animal) { return animal.sound(); }

function Cat(name) { this.name = name; this.sound = () => "Meow!"; }
Cat.prototype = Object.create({ sound: () => "Generic Animal Sound"});
function Animal(name) { this.name = name; this.sound = () => "Generic Animal Sound"; }

let cats = [new Cat("Whiskers"), new Cat("Mittens")];

//See kood viskab vea, sest Animal tüübi määramine Cat massiivile ei ole korrektne
//modifyAnimals(cats, (animal) => new Animal("Generic")); 

//See töötab, sest Cat tüüp määratakse Cat massiivile
modifyAnimals(cats, (cat) => new Cat("Fuzzy"));

//cats.forEach(cat => console.log(cat.sound()));

Kuigi see JavaScripti näide näitab otse potentsiaalset probleemi, TypeScripti tüübisüsteem üldiselt *väldib* sellist otsest määramist. Variatsiooni kaalutlused muutuvad oluliseks keerukamates stsenaariumides, eriti funktsioonitüüpide ja geneeriliste liidestega tegelemisel.

Tüübiparameetrite piirangud

Tüübiparameetrite piirangud võimaldavad teil piirata tüüpe, mida saab kasutada tüübiargumentidena geneerilistes tüüpides ja funktsioonides. Need pakuvad viisi tüüpidevaheliste seoste väljendamiseks ja teatud omaduste jõustamiseks. See on võimas mehhanism tüübiohutuse tagamiseks ja täpsema tüübimääratluse võimaldamiseks.

extends märksõna

Peamine viis tüübiparameetrite piirangute defineerimiseks on kasutada extends märksõna. See märksõna määrab, et tüübiparameeter peab olema teatud tüübi alamtüüp.

function logName<T extends { name: string }>(obj: T): void {
  console.log(obj.name);
}

// Kehtiv kasutus
logName({ name: "Alice", age: 30 });

// Viga: Argument tüübiga '{}' ei ole määratav parameetrile tüübiga '{ name: string; }'.
// logName({});

Selles näites on tüübiparameeter T piiratud olema tüüp, millel on name omadus tüübiga string. See tagab, et logName funktsioon saab turvaliselt ligi pääseda oma argumendi name omadusele.

Mitu piirangut ühisosatüüpidega

Saate kombineerida mitu piirangut, kasutades ühisosatüüpe (&). See võimaldab teil määrata, et tüübiparameeter peab vastama mitmele tingimusele.

interface Named {
  name: string;
}

interface Aged {
  age: number;
}

function logPerson<T extends Named & Aged>(person: T): void {
  console.log(`Name: ${person.name}, Age: ${person.age}`);
}

// Kehtiv kasutus
logPerson({ name: "Bob", age: 40 });

// Viga: Argument tüübiga '{ name: string; }' ei ole määratav parameetrile tüübiga 'Named & Aged'.
// Omadus 'age' puudub tüübis '{ name: string; }', kuid on nõutud tüübis 'Aged'.
// logPerson({ name: "Charlie" });

Siin on tüübiparameeter T piiratud olema tüüp, mis on nii Named kui ka Aged. See tagab, et logPerson funktsioon saab turvaliselt ligi pääseda nii name kui ka age omadustele.

Tüübipiirangute kasutamine geneeriliste klassidega

Tüübipiirangud on sama kasulikud ka geneeriliste klassidega töötamisel.

interface Printable {
  print(): void;
}

class Document<T extends Printable> {
  content: T;

  constructor(content: T) {
    this.content = content;
  }

  printDocument(): void {
    this.content.print();
  }
}

class Invoice implements Printable {
  invoiceNumber: string;

  constructor(invoiceNumber: string) {
    this.invoiceNumber = invoiceNumber;
  }

  print(): void {
    console.log(`Printing invoice: ${this.invoiceNumber}`);
  }
}

const myInvoice = new Invoice("INV-2023-123");
const document = new Document(myInvoice);
document.printDocument(); // Väljund: Printing invoice: INV-2023-123

Selles näites on Document klass geneeriline, kuid tüübiparameeter T on piiratud olema tüüp, mis implementeerib Printable liidest. See tagab, et igal objektil, mida kasutatakse Document klassi content'ina, on print meetod. See on eriti kasulik rahvusvahelistes kontekstides, kus printimine võib hõlmata erinevaid formaate või keeli, nõudes ühist print liidest.

Kovariantsus, kontravariantsus ja invariantsus TypeScriptis (uuesti vaadelduna)

Kuigi TypeScriptil ei ole selgesõnalisi variatsiooni annotatsioone (nagu in ja out mõnes teises keeles), käsitleb see variatsiooni kaudselt, tuginedes sellele, kuidas tüübiparameetreid kasutatakse. Oluline on mõista selle toimimise nüansse, eriti funktsiooniparameetrite puhul.

Funktsiooni parameetrite tüübid: kontravariantsus

Funktsiooni parameetrite tüübid on kontravariantsed. See tähendab, et saate turvaliselt edastada funktsiooni, mis aktsepteerib oodatust üldisemat tüüpi. See on sellepärast, et kui funktsioon suudab käsitleda Supertype'i, suudab see kindlasti käsitleda ka Subtype'i.

interface Animal {
  name: string;
}

interface Cat extends Animal {
  meow(): void;
}

function feedAnimal(animal: Animal): void {
  console.log(`Feeding ${animal.name}`);
}

function feedCat(cat: Cat): void {
  console.log(`Feeding ${cat.name} (a cat)`);
  cat.meow();
}

// See on kehtiv, kuna funktsiooni parameetrite tüübid on kontravariantsed
let feed: (animal: Animal) => void = feedCat; 

let genericAnimal:Animal = {name: "Generic Animal"};

feed(genericAnimal); // Töötab, kuid ei tee "mäu"

let mittens: Cat = { name: "Mittens", meow: () => {console.log("Mittens meows");}};

feed(mittens); // Töötab samuti ja *võib* teha "mäu" olenevalt tegelikust funktsioonist.

Selles näites on feedCat alamtüüp tüübist (animal: Animal) => void. See on sellepärast, et feedCat aktsepteerib spetsiifilisemat tüüpi (Cat), muutes selle kontravariantseks Animal tüübi suhtes funktsiooniparameetris. Oluline osa on määramine: let feed: (animal: Animal) => void = feedCat; on kehtiv.

Tagastustüübid: kovariantsus

Funktsiooni tagastustüübid on kovariantsed. See tähendab, et saate turvaliselt tagastada oodatust spetsiifilisema tüübi. Kui funktsioon lubab tagastada Animal, on Cat tagastamine täiesti vastuvõetav.

function getAnimal(): Animal {
  return { name: "Generic Animal" };
}

function getCat(): Cat {
  return { name: "Whiskers", meow: () => { console.log("Whiskers meows"); } };
}

// See on kehtiv, kuna funktsiooni tagastustüübid on kovariantsed
let get: () => Animal = getCat;

let myAnimal: Animal = get();

console.log(myAnimal.name); // Töötab

// myAnimal.meow();  // Viga: Omadust 'meow' ei eksisteeri tüübil 'Animal'.
// Kassi-spetsiifiliste omaduste kasutamiseks on vaja kasutada tüübikinnitust

if ((myAnimal as Cat).meow) {
  (myAnimal as Cat).meow(); // Whiskers meows
}

Siin on getCat alamtüüp tüübist () => Animal, kuna see tagastab spetsiifilisema tüübi (Cat). Määramine let get: () => Animal = getCat; on kehtiv.

Massiivid ja geneerikud: invariantsus (enamasti)

TypeScript käsitleb massiive ja enamikku geneerilisi tüüpe vaikimisi invariantsetena. See tähendab, et Array<Cat> *ei ole* Array<Animal> alamtüüp, isegi kui Cat laiendab Animal. See on teadlik disainivalik, et vältida potentsiaalseid käitusaja vigu. Kuigi massiivid *käituvad* paljudes teistes keeltes kovariantsetena, teeb TypeScript need ohutuse huvides invariantseks.

let animals: Animal[] = [{ name: "Generic Animal" }];
let cats: Cat[] = [{ name: "Whiskers", meow: () => { console.log("Whiskers meows"); } }];

// Viga: Tüüp 'Cat[]' ei ole määratav tüübile 'Animal[]'.
// Tüüp 'Cat' ei ole määratav tüübile 'Animal'.
// Omadus 'meow' puudub tüübis 'Animal', kuid on nõutud tüübis 'Cat'.
// animals = cats; // See põhjustaks probleeme, kui see oleks lubatud!

//Kuid see töötab
animals[0] = cats[0];

console.log(animals[0].name);

//animals[0].meow();  // viga - animals[0] nähakse kui tüüpi Animal, seega meow pole saadaval

(animals[0] as Cat).meow(); // Kassi-spetsiifiliste meetodite kasutamiseks on vaja tüübikinnitust

Määramise animals = cats; lubamine oleks ohtlik, sest siis saaksite lisada geneerilise Animal tüübi animals massiivi, mis rikuks cats massiivi tüübiohutust (mis peaks sisaldama ainult Cat objekte). Seetõttu järeldab TypeScript, et massiivid on invariantsed.

Praktilised näited ja kasutusjuhud

Geneeriline repositooriumi muster

Vaatleme geneerilist repositooriumi mustrit andmetele juurdepääsuks. Teil võib olla baasolemi tüüp ja geneeriline repositooriumi liides, mis töötab selle tüübiga.

interface Entity {
  id: string;
}

interface Repository<T extends Entity> {
  getById(id: string): T | undefined;
  save(entity: T): void;
  delete(id: string): void;
}

class InMemoryRepository<T extends Entity> implements Repository<T> {
  private data: { [id: string]: T } = {};

  getById(id: string): T | undefined {
    return this.data[id];
  }

  save(entity: T): void {
    this.data[entity.id] = entity;
  }

  delete(id: string): void {
    delete this.data[id];
  }
}

interface Product extends Entity {
  name: string;
  price: number;
}

const productRepository: Repository<Product> = new InMemoryRepository<Product>();

const newProduct: Product = { id: "123", name: "Laptop", price: 1200 };
productRepository.save(newProduct);

const retrievedProduct = productRepository.getById("123");
if (retrievedProduct) {
  console.log(`Leitud toode: ${retrievedProduct.name}`);
}

Tüübipiirang T extends Entity tagab, et repositoorium saab töötada ainult olemitega, millel on id omadus. See aitab säilitada andmete terviklikkust ja järjepidevust. See muster on kasulik erinevates formaatides andmete haldamiseks, kohandudes rahvusvahelistumisega, käsitledes erinevaid valuutatüüpe Product liidese sees.

Sündmuste käsitlemine geneeriliste andmekoormatega

Teine levinud kasutusjuht on sündmuste käsitlemine. Saate defineerida geneerilise sündmuse tüübi spetsiifilise andmekoormaga.

interface Event<T> {
  type: string;
  payload: T;
}

interface UserCreatedEventPayload {
  userId: string;
  email: string;
}

interface ProductPurchasedEventPayload {
  productId: string;
  quantity: number;
}

function handleEvent<T>(event: Event<T>): void {
  console.log(`Käsitlen sündmust tüübiga: ${event.type}`);
  console.log(`Andmekoorem: ${JSON.stringify(event.payload)}`);
}

const userCreatedEvent: Event<UserCreatedEventPayload> = {
  type: "user.created",
  payload: { userId: "user123", email: "alice@example.com" },
};

const productPurchasedEvent: Event<ProductPurchasedEventPayload> = {
  type: "product.purchased",
  payload: { productId: "product456", quantity: 2 },
};

handleEvent(userCreatedEvent);
handleEvent(productPurchasedEvent);

See võimaldab teil defineerida erinevaid sündmusetüüpe erinevate andmekoorma struktuuridega, säilitades samal ajal tüübiohutuse. Seda struktuuri saab hõlpsasti laiendada, et toetada lokaliseeritud sündmuste üksikasju, lisades sündmuse andmekoormasse piirkondlikke eelistusi, nagu erinevad kuupäevavormingud või keelespetsiifilised kirjeldused.

Geneerilise andmete teisendamise konveieri ehitamine

Kujutage ette stsenaariumi, kus peate andmeid ühest formaadist teise teisendama. Geneerilise andmete teisendamise konveieri saab implementeerida, kasutades tüübiparameetrite piiranguid, et tagada sisend- ja väljundtüüpide ühilduvus teisendusfunktsioonidega.

interface DataTransformer<TInput, TOutput> {
  transform(input: TInput): TOutput;
}

function processData<TInput, TOutput, TIntermediate>(
  input: TInput,
  transformer1: DataTransformer<TInput, TIntermediate>,
  transformer2: DataTransformer<TIntermediate, TOutput>
): TOutput {
  const intermediateData = transformer1.transform(input);
  const outputData = transformer2.transform(intermediateData);
  return outputData;
}

interface RawUserData {
  firstName: string;
  lastName: string;
}

interface UserData {
  fullName: string;
  email: string;
}

class RawToIntermediateTransformer implements DataTransformer<RawUserData, {name: string}> {
    transform(input: RawUserData): {name: string} {
        return { name: `${input.firstName} ${input.lastName}`};
    }
}

class IntermediateToUserTransformer implements DataTransformer<{name: string}, UserData> {
    transform(input: {name: string}): UserData {
        return {fullName: input.name, email: `${input.name.replace(" ", ".")}@example.com`};
    }
}

const rawData: RawUserData = { firstName: "John", lastName: "Doe" };

const userData: UserData = processData(
  rawData,
  new RawToIntermediateTransformer(),
  new IntermediateToUserTransformer()
);

console.log(userData);

Selles näites võtab funktsioon processData sisendi, kaks teisendajat ja tagastab teisendatud väljundi. Tüübiparameetrid ja piirangud tagavad, et esimese teisendaja väljund ühildub teise teisendaja sisendiga, luues tüübiohutu konveieri. See muster võib olla hindamatu rahvusvaheliste andmekogumitega tegelemisel, millel on erinevad väljanimed või andmestruktuurid, kuna saate iga vormingu jaoks ehitada spetsiifilisi teisendajaid.

Parimad praktikad ja kaalutlused

Kokkuvõte

TypeScript'i variatsiooni annotatsioonide (kaudselt funktsiooni parameetrite reeglite kaudu) ja tüübiparameetrite piirangute meisterlik valdamine on oluline robustse, paindliku ja hooldatava koodi loomiseks. Mõistes kovariantsuse, kontravariantsuse ja invariantsuse kontseptsioone ning kasutades tüübipiiranguid tõhusalt, saate kirjutada geneerilist koodi, mis on nii tüübiohutu kui ka korduvkasutatav. Need tehnikad on eriti väärtuslikud rakenduste arendamisel, mis peavad käsitlema mitmekesiseid andmetüüpe või kohanema erinevate keskkondadega, nagu on tänapäeva globaliseerunud tarkvaramaastikul tavaline. Järgides parimaid praktikaid ja testides oma koodi põhjalikult, saate avada TypeScripti tüübisüsteemi täieliku potentsiaali ja luua kvaliteetset tarkvara.