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:
- Kovariantsus: Geneeriline tüüp
Container<T>
on kovariantne, kuiContainer<Subtype>
onContainer<Supertype>
alamtüüp alati, kuiSubtype
onSupertype
alamtüüp. Mõelge sellele kui alamtüübi suhte säilitamisele. Paljudes keeltes (kuigi mitte otse TypeScripti funktsiooniparameetrites) on geneerilised massiivid kovariantsed. Näiteks, kuiCat
laiendabAnimal
, siis `Array<Cat>` *käitub* nagu see oleks `Array<Animal>` alamtüüp (kuigi TypeScripti tüübisüsteem väldib selgesõnalist kovariantsust, et ennetada käitusaja vigu). - Kontravariantsus: Geneeriline tüüp
Container<T>
on kontravariantne, kuiContainer<Supertype>
onContainer<Subtype>
alamtüüp alati, kuiSubtype
onSupertype
alamtüüp. See pöörab alamtüübi suhte vastupidiseks. Funktsiooniparameetrite tüübid on kontravariantsed. - Invariantsus: Geneeriline tüüp
Container<T>
on invariantne, kuiContainer<Subtype>
ei ole ei alamtüüp ega ülaltüüp tüübileContainer<Supertype>
, isegi kuiSubtype
onSupertype
alamtüüp. TypeScripti geneerilised tüübid on üldiselt invariantsed, kui pole teisiti määratud (kaudselt, funktsiooniparameetrite reeglite kaudu kontravariantsuse jaoks).
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
- Eelistage kompositsiooni pärilusele: Kuigi pärilus võib olla kasulik, eelistage kompositsiooni ja liideseid suurema paindlikkuse ja hooldatavuse saavutamiseks, eriti keeruliste tüübisuhete puhul.
- Kasutage tüübipiiranguid läbimõeldult: Ärge piirake tüübiparameetreid üleliia. Püüdke kasutada kõige üldisemaid tüüpe, mis pakuvad endiselt vajalikku tüübiohutust.
- Kaaluge jõudluse mõjusid: Geneerikute liigne kasutamine võib mõnikord jõudlust mõjutada. Profileerige oma koodi, et tuvastada võimalikud kitsaskohad.
- Dokumenteerige oma kood: Dokumenteerige selgelt oma geneeriliste tüüpide ja tüübipiirangute eesmärk. See muudab teie koodi lihtsamini mõistetavaks ja hooldatavaks.
- Testige põhjalikult: Kirjutage põhjalikud ühiktestid, et tagada teie geneerilise koodi ootuspärane käitumine erinevate tüüpidega.
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.