Latviešu

Atklājiet TypeScript variācijas anotāciju spēku, lai radītu elastīgāku, drošāku un uzturamāku kodu. Dziļa analīze ar praktiskiem piemēriem.

TypeScript variācijas anotācijas: tipa parametru ierobežojumu apgūšana robusta koda izveidei

TypeScript, kas ir JavaScript virskopa, nodrošina statisku tipēšanu, uzlabojot koda uzticamību un uzturējamību. Viena no TypeScript progresīvākajām, bet jaudīgākajām funkcijām ir tās atbalsts variācijas anotācijām apvienojumā ar tipa parametru ierobežojumiem. Šo jēdzienu izpratne ir būtiska, lai rakstītu patiesi robustu un elastīgu vispārīgo kodu. Šis emuāra ieraksts iedziļināsies variācijā, kovariācijā, kontravariācijā un invariācijā, izskaidrojot, kā efektīvi izmantot tipa parametru ierobežojumus, lai veidotu drošākus un atkārtoti lietojamus komponentus.

Izpratne par variāciju

Variācija apraksta, kā apakštipa attiecības starp tipiem ietekmē apakštipa attiecības starp konstruētiem tipiem (piemēram, vispārīgiem tipiem). Apskatīsim galvenos terminus:

Visvieglāk to atcerēties ar analoģiju: iedomājieties rūpnīcu, kas ražo suņu kaklasiksnas. Kovarianta rūpnīca varētu ražot kaklasiksnas visu veidu dzīvniekiem, ja tā spēj ražot kaklasiksnas suņiem, saglabājot apakštipa attiecību. Kontravarianta rūpnīca ir tāda, kas var *patērēt* jebkura veida dzīvnieku kaklasiksnas, ja tā spēj patērēt suņu kaklasiksnas. Ja rūpnīca var strādāt tikai ar suņu kaklasiksnām un neko citu, tā ir invarianta attiecībā pret dzīvnieka tipu.

Kāpēc variācija ir svarīga?

Variācijas izpratne ir būtiska, lai rakstītu tipdrošu kodu, īpaši strādājot ar vispārīgiem tipiem. Nepareiza kovariācijas vai kontravariācijas pieņemšana var novest pie izpildlaika kļūdām, kuras TypeScript tipu sistēma ir izstrādāta, lai novērstu. Apsveriet šo kļūdaino piemēru (JavaScript valodā, bet tas ilustrē jēdzienu):

// JavaScript piemērs (tikai ilustratīvs, NAV 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")];

//Šis kods izraisīs kļūdu, jo Animal piešķiršana Cat masīvam nav pareiza
//modifyAnimals(cats, (animal) => new Animal("Generic")); 

//Šis darbojas, jo Cat tiek piešķirts Cat masīvam
modifyAnimals(cats, (cat) => new Cat("Fuzzy"));

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

Lai gan šis JavaScript piemērs tieši parāda potenciālo problēmu, TypeScript tipu sistēma parasti *novērš* šāda veida tiešu piešķiršanu. Variācijas apsvērumi kļūst svarīgi sarežģītākos scenārijos, īpaši strādājot ar funkciju tipiem un vispārīgām saskarnēm.

Tipa parametru ierobežojumi

Tipa parametru ierobežojumi ļauj ierobežot tipus, kurus var izmantot kā tipu argumentus vispārīgos tipos un funkcijās. Tie nodrošina veidu, kā izteikt attiecības starp tipiem un ieviest noteiktas īpašības. Tas ir spēcīgs mehānisms tipu drošības nodrošināšanai un precīzākas tipu secināšanas veicināšanai.

Atslēgvārds extends

Galvenais veids, kā definēt tipa parametru ierobežojumus, ir izmantot atslēgvārdu extends. Šis atslēgvārds norāda, ka tipa parametram ir jābūt noteikta tipa apakštipam.

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

// Derīga lietošana
logName({ name: "Alice", age: 30 });

// Kļūda: Argumenta tips '{}' nav piešķirams parametra tipam '{ name: string; }'.
// logName({});

Šajā piemērā tipa parametrs T ir ierobežots kā tips, kuram ir īpašība name ar tipu string. Tas nodrošina, ka funkcija logName var droši piekļūt sava argumenta īpašībai name.

Vairāki ierobežojumi ar krustojuma tipiem (Intersection Types)

Jūs varat apvienot vairākus ierobežojumus, izmantojot krustojuma tipus (&). Tas ļauj norādīt, ka tipa parametram ir jāatbilst vairākiem nosacījumiem.

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}`);
}

// Derīga lietošana
logPerson({ name: "Bob", age: 40 });

// Kļūda: Argumenta tips '{ name: string; }' nav piešķirams parametra tipam 'Named & Aged'.
// Īpašība 'age' trūkst tipā '{ name: string; }', bet ir nepieciešama tipā 'Aged'.
// logPerson({ name: "Charlie" });

Šeit tipa parametrs T ir ierobežots kā tips, kas ir gan Named, gan Aged. Tas nodrošina, ka funkcija logPerson var droši piekļūt gan īpašībai name, gan age.

Tipu ierobežojumu izmantošana ar vispārīgām klasēm

Tipu ierobežojumi ir vienlīdz noderīgi, strādājot ar vispārīgām klasēm.

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(); // Izvade: Printing invoice: INV-2023-123

Šajā piemērā klase Document ir vispārīga, bet tās tipa parametrs T ir ierobežots kā tips, kas implementē saskarni Printable. Tas garantē, ka jebkuram objektam, kas tiek izmantots kā Document saturs (content), būs metode print. Tas ir īpaši noderīgi starptautiskos kontekstos, kur drukāšana var ietvert dažādus formātus vai valodas, prasot kopīgu print saskarni.

Kovariācija, kontravariācija un invariācija TypeScript (pārskats)

Lai gan TypeScript nav tiešu variācijas anotāciju (kā in un out dažās citās valodās), tas netieši pārvalda variāciju, pamatojoties uz to, kā tiek izmantoti tipu parametri. Ir svarīgi izprast nianses, kā tas darbojas, īpaši ar funkciju parametriem.

Funkciju parametru tipi: Kontravariācija

Funkciju parametru tipi ir kontravarianti. Tas nozīmē, ka jūs varat droši nodot funkciju, kas pieņem vispārīgāku tipu, nekā gaidīts. Tas ir tāpēc, ka, ja funkcija var apstrādāt Supertype, tā noteikti var apstrādāt arī Subtype.

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();
}

// Tas ir derīgi, jo funkciju parametru tipi ir kontravarianti
let feed: (animal: Animal) => void = feedCat; 

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

feed(genericAnimal); // Darbojas, bet neņaudēs

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

feed(mittens); // Arī darbojas, un *varētu* ņaudēt atkarībā no faktiskās funkcijas.

Šajā piemērā feedCat ir (animal: Animal) => void apakštips. Tas ir tāpēc, ka feedCat pieņem specifiskāku tipu (Cat), padarot to kontravariantu attiecībā pret Animal tipu funkcijas parametrā. Būtiskākā daļa ir piešķiršana: let feed: (animal: Animal) => void = feedCat; ir derīga.

Atgriešanas tipi: Kovariācija

Funkciju atgriešanas tipi ir kovarianti. Tas nozīmē, ka jūs varat droši atgriezt specifiskāku tipu, nekā gaidīts. Ja funkcija sola atgriezt Animal, ir pilnīgi pieņemami atgriezt Cat.

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

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

// Tas ir derīgi, jo funkciju atgriešanas tipi ir kovarianti
let get: () => Animal = getCat;

let myAnimal: Animal = get();

console.log(myAnimal.name); // Darbojas

// myAnimal.meow();  // Kļūda: Īpašība 'meow' nepastāv tipā 'Animal'.
// Lai piekļūtu Cat specifiskajām īpašībām, ir jāizmanto tipa apgalvojums (type assertion)

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

Šeit getCat ir () => Animal apakštips, jo tas atgriež specifiskāku tipu (Cat). Piešķiršana let get: () => Animal = getCat; ir derīga.

Masīvi un vispārīgie tipi: Invariācija (pārsvarā)

TypeScript pēc noklusējuma apstrādā masīvus un lielāko daļu vispārīgo tipu kā invariantus. Tas nozīmē, ka Array<Cat> *netiek* uzskatīts par Array<Animal> apakštipu, pat ja Cat paplašina Animal. Šis ir apzināts dizaina lēmums, lai novērstu potenciālas izpildlaika kļūdas. Lai gan masīvi daudzās citās valodās *uzvedas* kā kovarianti, TypeScript drošības nolūkos tos padara par invariantiem.

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

// Kļūda: Tips 'Cat[]' nav piešķirams tipam 'Animal[]'.
// Tips 'Cat' nav piešķirams tipam 'Animal'.
// Īpašība 'meow' trūkst tipā 'Animal', bet ir nepieciešama tipā 'Cat'.
// animals = cats; // Ja tas būtu atļauts, tas radītu problēmas!

//Tomēr šis darbosies
animals[0] = cats[0];

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

//animals[0].meow();  // kļūda - animals[0] tiek uztverts kā tips Animal, tāpēc meow nav pieejams

(animals[0] as Cat).meow(); // Lai izmantotu Cat specifiskās metodes, nepieciešams tipa apgalvojums

Atļaut piešķiršanu animals = cats; būtu nedroši, jo tad jūs varētu pievienot vispārīgu Animal masīvam animals, kas pārkāptu masīva cats tipu drošību (kuram vajadzētu saturēt tikai Cat objektus). Šī iemesla dēļ TypeScript secina, ka masīvi ir invarianti.

Praktiski piemēri un lietošanas gadījumi

Vispārīgais repozitorija šablons (Generic Repository Pattern)

Apsveriet vispārīgo repozitorija šablonu datu piekļuvei. Jums varētu būt bāzes entītijas tips un vispārīga repozitorija saskarne, kas darbojas ar šo tipu.

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(`Retrieved product: ${retrievedProduct.name}`);
}

Tipa ierobežojums T extends Entity nodrošina, ka repozitorijs var darboties tikai ar entītijām, kurām ir īpašība id. Tas palīdz uzturēt datu integritāti un konsekvenci. Šis šablons ir noderīgs, lai pārvaldītu datus dažādos formātos, pielāgojoties internacionalizācijai, apstrādājot dažādus valūtu tipus Product saskarnē.

Notikumu apstrāde ar vispārīgiem datiem (Payloads)

Vēl viens izplatīts lietošanas gadījums ir notikumu apstrāde. Jūs varat definēt vispārīgu notikuma tipu ar specifisku datu kravu (payload).

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(`Handling event of type: ${event.type}`);
  console.log(`Payload: ${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);

Tas ļauj definēt dažādus notikumu tipus ar dažādām datu struktūrām, vienlaikus saglabājot tipu drošību. Šo struktūru var viegli paplašināt, lai atbalstītu lokalizētu notikumu informāciju, iekļaujot notikuma datos reģionālās preferences, piemēram, dažādus datuma formātus vai valodai specifiskus aprakstus.

Vispārīgas datu transformācijas konveijera izveide

Apsveriet scenāriju, kurā nepieciešams pārveidot datus no viena formāta citā. Vispārīgu datu transformācijas konveijeru var ieviest, izmantojot tipa parametru ierobežojumus, lai nodrošinātu, ka ievades un izvades tipi ir saderīgi ar transformācijas funkcijām.

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);

Šajā piemērā funkcija processData saņem ievadi, divus transformatorus un atgriež transformēto izvadi. Tipu parametri un ierobežojumi nodrošina, ka pirmā transformatora izvade ir saderīga ar otrā transformatora ievadi, veidojot tipdrošu konveijeru. Šis šablons var būt nenovērtējams, strādājot ar starptautiskām datu kopām, kurām ir atšķirīgi lauku nosaukumi vai datu struktūras, jo jūs varat izveidot specifiskus transformatorus katram formātam.

Labākās prakses un apsvērumi

Noslēgums

TypeScript variācijas anotāciju (netieši caur funkciju parametru noteikumiem) un tipa parametru ierobežojumu apgūšana ir būtiska, lai veidotu robustu, elastīgu un uzturamu kodu. Izprotot kovariācijas, kontravariācijas un invariācijas jēdzienus un efektīvi izmantojot tipu ierobežojumus, jūs varat rakstīt vispārīgu kodu, kas ir gan tipdrošs, gan atkārtoti lietojams. Šīs metodes ir īpaši vērtīgas, izstrādājot lietojumprogrammas, kurām jāapstrādā dažādi datu tipi vai jāpielāgojas dažādām vidēm, kas ir raksturīgi mūsdienu globalizētajā programmatūras ainavā. Ievērojot labākās prakses un rūpīgi testējot kodu, jūs varat pilnībā atraisīt TypeScript tipu sistēmas potenciālu un radīt augstas kvalitātes programmatūru.