Atskleiskite TypeScript variantiškumo anotacijų galią, kad sukurtumėte lankstesnį, saugesnį ir lengviau prižiūrimą kodą. Išsami analizė su pavyzdžiais.
TypeScript variantiškumo anotacijos: tipo parametrų apribojimų įvaldymas tvirtam kodui
TypeScript, JavaScript viršaibis, suteikia statinį tipizavimą, pagerindamas kodo patikimumą ir priežiūrą. Viena iš pažangesnių, tačiau galingų TypeScript funkcijų yra variantiškumo anotacijų palaikymas kartu su tipo parametrų apribojimais. Šių koncepcijų supratimas yra labai svarbus norint rašyti tikrai tvirtą ir lankstų generinį kodą. Šiame tinklaraščio įraše gilinsimės į variantiškumą, kovariantiškumą, kontravariantiškumą ir invariantiškumą, paaiškindami, kaip efektyviai naudoti tipo parametrų apribojimus, siekiant sukurti saugesnius ir labiau pakartotinai naudojamus komponentus.
Variantiškumo supratimas
Variantiškumas apibūdina, kaip potipio ryšys tarp tipų veikia potipio ryšį tarp sudarytų tipų (pvz., generinių tipų). Išsiaiškinkime pagrindinius terminus:
- Kovariantiškumas: Generinis tipas
Container<T>
yra kovariantinis, jeiContainer<Subtype>
yraContainer<Supertype>
potipis, kaiSubtype
yraSupertype
potipis. Galvokite apie tai kaip apie potipio ryšio išsaugojimą. Daugelyje kalbų (nors ne tiesiogiai TypeScript funkcijų parametruose) generiniai masyvai yra kovariantiniai. Pavyzdžiui, jeiCat
išplečiaAnimal
, tuomet `Array<Cat>` *elgiasi* taip, lyg būtų `Array<Animal>` potipis (nors TypeScript tipų sistema vengia aiškaus kovariantiškumo, kad išvengtų vykdymo laiko klaidų). - Kontravariantiškumas: Generinis tipas
Container<T>
yra kontravariantinis, jeiContainer<Supertype>
yraContainer<Subtype>
potipis, kaiSubtype
yraSupertype
potipis. Tai apverčia potipio ryšį. Funkcijų parametrų tipai pasižymi kontravariantiškumu. - Invariantiškumas: Generinis tipas
Container<T>
yra invariantinis, jeiContainer<Subtype>
nėra neiContainer<Supertype>
potipis, nei virštipis, net jeiSubtype
yraSupertype
potipis. TypeScript generiniai tipai paprastai yra invariantiniai, nebent nurodyta kitaip (netiesiogiai, per funkcijų parametrų taisykles kontravariantiškumui).
Lengviausia prisiminti pasitelkus analogiją: Įsivaizduokite gamyklą, gaminančią šunų antkaklius. Kovariantinė gamykla galėtų gaminti antkaklius visų tipų gyvūnams, jei ji gali gaminti antkaklius šunims, išsaugodama potipio ryšį. Kontravariantinė gamykla yra ta, kuri gali *suvartoti* bet kokio tipo gyvūnų antkaklius, jei ji gali suvartoti šunų antkaklius. Jei gamykla gali dirbti tik su šunų antkakliais ir niekuo kitu, ji yra invariantinė gyvūno tipui.
Kodėl variantiškumas yra svarbus?
Variantiškumo supratimas yra labai svarbus rašant tipų atžvilgiu saugų kodą, ypač dirbant su generiniais tipais. Neteisingas kovariantiškumo ar kontravariantiškumo numatymas gali sukelti vykdymo laiko klaidas, kurių TypeScript tipų sistema yra skirta išvengti. Apsvarstykite šį klaidingą pavyzdį (JavaScript kalboje, bet iliustruojantį koncepciją):
// JavaScript pavyzdys (tik iliustracijai, NE 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 kodas sukels klaidą, nes priskirti Animal į Cat masyvą yra neteisinga
//modifyAnimals(cats, (animal) => new Animal("Generic"));
//Tai veikia, nes Cat yra priskiriamas Cat masyvui
modifyAnimals(cats, (cat) => new Cat("Fuzzy"));
//cats.forEach(cat => console.log(cat.sound()));
Nors šis JavaScript pavyzdys tiesiogiai parodo galimą problemą, TypeScript tipų sistema paprastai *neleidžia* tokio tiesioginio priskyrimo. Variantiškumo aspektai tampa svarbūs sudėtingesniuose scenarijuose, ypač dirbant su funkcijų tipais ir generinėmis sąsajomis.
Tipo parametrų apribojimai
Tipo parametrų apribojimai leidžia apriboti tipus, kurie gali būti naudojami kaip tipo argumentai generiniuose tipuose ir funkcijose. Jie suteikia būdą išreikšti ryšius tarp tipų ir įgyvendinti tam tikras savybes. Tai yra galingas mechanizmas, užtikrinantis tipų saugumą ir leidžiantis tikslesnį tipų išvedimą.
Raktinis žodis extends
Pagrindinis būdas apibrėžti tipo parametrų apribojimus yra naudojant raktinį žodį extends
. Šis raktinis žodis nurodo, kad tipo parametras turi būti tam tikro tipo potipis.
function logName<T extends { name: string }>(obj: T): void {
console.log(obj.name);
}
// Teisingas naudojimas
logName({ name: "Alice", age: 30 });
// Klaida: Argumentas, kurio tipas '{}', negali būti priskirtas parametrui, kurio tipas '{ name: string; }'.
// logName({});
Šiame pavyzdyje tipo parametras T
yra apribotas, kad būtų tipas, turintis savybę name
, kurios tipas yra string
. Tai užtikrina, kad funkcija logName
gali saugiai pasiekti savo argumento savybę name
.
Keli apribojimai su sankirtos tipais
Galite sujungti kelis apribojimus naudodami sankirtos tipus (&
). Tai leidžia nurodyti, kad tipo parametras turi atitikti kelias sąlygas.
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}`);
}
// Teisingas naudojimas
logPerson({ name: "Bob", age: 40 });
// Klaida: Argumentas, kurio tipas '{ name: string; }', negali būti priskirtas parametrui, kurio tipas 'Named & Aged'.
// Savybė 'age' trūksta tipe '{ name: string; }', bet yra privaloma tipe 'Aged'.
// logPerson({ name: "Charlie" });
Čia tipo parametras T
yra apribotas, kad būtų tipas, kuris yra ir Named
, ir Aged
. Tai užtikrina, kad funkcija logPerson
gali saugiai pasiekti tiek name
, tiek age
savybes.
Tipo apribojimų naudojimas su generinėmis klasėmis
Tipo apribojimai yra vienodai naudingi dirbant su generinėmis klasėmis.
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(); // Išvestis: Printing invoice: INV-2023-123
Šiame pavyzdyje Document
klasė yra generinė, bet tipo parametras T
yra apribotas, kad būtų tipas, kuris įgyvendina Printable
sąsają. Tai garantuoja, kad bet koks objektas, naudojamas kaip Document
turinys (content
), turės print
metodą. Tai ypač naudinga tarptautiniuose kontekstuose, kur spausdinimas gali apimti įvairius formatus ar kalbas, reikalaujant bendros print
sąsajos.
Kovariantiškumas, kontravariantiškumas ir invariantiškumas TypeScript kalboje (peržvelgta)
Nors TypeScript neturi aiškių variantiškumo anotacijų (kaip in
ir out
kai kuriose kitose kalbose), jis netiesiogiai tvarko variantiškumą atsižvelgdamas į tai, kaip naudojami tipo parametrai. Svarbu suprasti niuansus, kaip tai veikia, ypač su funkcijų parametrais.
Funkcijų parametrų tipai: Kontravariantiškumas
Funkcijų parametrų tipai yra kontravariantiniai. Tai reiškia, kad galite saugiai perduoti funkciją, kuri priima bendresnį tipą, nei tikėtasi. Taip yra todėl, kad jei funkcija gali apdoroti Supertype
, ji tikrai gali apdoroti ir 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();
}
// Tai yra teisinga, nes funkcijų parametrų tipai yra kontravariantiniai
let feed: (animal: Animal) => void = feedCat;
let genericAnimal:Animal = {name: "Generic Animal"};
feed(genericAnimal); // Veikia, bet nemiauksės
let mittens: Cat = { name: "Mittens", meow: () => {console.log("Mittens meows");}};
feed(mittens); // Taip pat veikia ir *gali* miauksėti, priklausomai nuo faktinės funkcijos.
Šiame pavyzdyje feedCat
yra (animal: Animal) => void
potipis. Taip yra todėl, kad feedCat
priima konkretesnį tipą (Cat
), todėl yra kontravariantinis atsižvelgiant į Animal
tipą funkcijos parametre. Svarbiausia dalis yra priskyrimas: let feed: (animal: Animal) => void = feedCat;
yra teisingas.
Grąžinamų reikšmių tipai: Kovariantiškumas
Funkcijų grąžinami tipai yra kovariantiniai. Tai reiškia, kad galite saugiai grąžinti konkretesnį tipą, nei tikėtasi. Jei funkcija žada grąžinti Animal
, grąžinti Cat
yra visiškai priimtina.
function getAnimal(): Animal {
return { name: "Generic Animal" };
}
function getCat(): Cat {
return { name: "Whiskers", meow: () => { console.log("Whiskers meows"); } };
}
// Tai yra teisinga, nes funkcijų grąžinami tipai yra kovariantiniai
let get: () => Animal = getCat;
let myAnimal: Animal = get();
console.log(myAnimal.name); // Veikia
// Klaida: Savybė 'meow' neegzistuoja tipe 'Animal'.
// myAnimal.meow();
// Reikia naudoti tipo tvirtinimą (type assertion), kad pasiektumėte Cat specifines savybes
if ((myAnimal as Cat).meow) {
(myAnimal as Cat).meow(); // Whiskers meows
}
Čia getCat
yra () => Animal
potipis, nes grąžina konkretesnį tipą (Cat
). Priskyrimas let get: () => Animal = getCat;
yra teisingas.
Masyvai ir generiniai tipai: Invariantiškumas (dažniausiai)
TypeScript masyvus ir daugumą generinių tipų pagal numatymą traktuoja kaip invariantinius. Tai reiškia, kad Array<Cat>
*nėra* laikomas Array<Animal>
potipiu, net jei Cat
išplečia Animal
. Tai yra sąmoningas dizaino sprendimas, siekiant išvengti galimų vykdymo laiko klaidų. Nors daugelyje kitų kalbų masyvai *elgiasi* taip, lyg būtų kovariantiniai, TypeScript saugumo sumetimais juos padaro invariantiniais.
let animals: Animal[] = [{ name: "Generic Animal" }];
let cats: Cat[] = [{ name: "Whiskers", meow: () => { console.log("Whiskers meows"); } }];
// Klaida: Tipas 'Cat[]' negali būti priskirtas tipui 'Animal[]'.
// Tipas 'Cat' negali būti priskirtas tipui 'Animal'.
// Savybė 'meow' trūksta tipe 'Animal', bet yra privaloma tipe 'Cat'.
// animals = cats; // Tai sukeltų problemų, jei būtų leista!
//Tačiau tai veiks
animals[0] = cats[0];
console.log(animals[0].name);
//animals[0].meow(); // klaida - animals[0] yra matomas kaip Animal tipas, todėl meow nepasiekiamas
(animals[0] as Cat).meow(); // Reikalingas tipo tvirtinimas, kad būtų galima naudoti Cat specifinius metodus
Leidžiant priskyrimą animals = cats;
būtų nesaugu, nes tada galėtumėte pridėti bendrinį Animal
į animals
masyvą, o tai pažeistų cats
masyvo tipų saugumą (kuris turėtų содержать tik Cat
objektus). Dėl šios priežasties TypeScript daro išvadą, kad masyvai yra invariantiniai.
Praktiniai pavyzdžiai ir naudojimo atvejai
Generinis saugyklos (Repository) šablonas
Apsvarstykite generinį saugyklos šabloną duomenų prieigai. Galite turėti bazinį esybės (entity) tipą ir generinę saugyklos sąsają, kuri veikia su tuo 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}`);
}
Tipo apribojimas T extends Entity
užtikrina, kad saugykla gali veikti tik su esybėmis, kurios turi id
savybę. Tai padeda palaikyti duomenų vientisumą ir nuoseklumą. Šis šablonas yra naudingas valdant duomenis įvairiais formatais, prisitaikant prie internacionalizacijos, tvarkant skirtingus valiutų tipus Product
sąsajoje.
Įvykių apdorojimas su generinėmis naudingosiomis apkrovomis (Payloads)
Kitas dažnas naudojimo atvejis yra įvykių apdorojimas. Galite apibrėžti generinį įvykio tipą su specifine naudingąja apkrova.
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);
Tai leidžia apibrėžti skirtingus įvykių tipus su skirtingomis naudingųjų apkrovų struktūromis, išlaikant tipų saugumą. Ši struktūra gali būti lengvai išplėsta, kad palaikytų lokalizuotas įvykių detales, įtraukiant regioninius nustatymus į įvykio naudingąją apkrovą, pvz., skirtingus datų formatus ar konkrečios kalbos aprašymus.
Generinio duomenų transformavimo konvejerio kūrimas
Apsvarstykite scenarijų, kai reikia transformuoti duomenis iš vieno formato į kitą. Generinis duomenų transformavimo konvejeris gali būti įgyvendintas naudojant tipo parametrų apribojimus, siekiant užtikrinti, kad įvesties ir išvesties tipai būtų suderinami su transformavimo funkcijomis.
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);
Šiame pavyzdyje processData
funkcija priima įvestį, du transformatorius ir grąžina transformuotą išvestį. Tipo parametrai ir apribojimai užtikrina, kad pirmojo transformatoriaus išvestis yra suderinama su antrojo transformatoriaus įvestimi, sukuriant tipų atžvilgiu saugų konvejerį. Šis šablonas gali būti neįkainojamas dirbant su tarptautiniais duomenų rinkiniais, kurie turi skirtingus laukų pavadinimus ar duomenų struktūras, nes galite sukurti specifinius transformatorius kiekvienam formatui.
Gerosios praktikos ir aspektai
- Teikite pirmenybę kompozicijai, o ne paveldėjimui: Nors paveldėjimas gali būti naudingas, pirmenybę teikite kompozicijai ir sąsajoms, kad pasiektumėte didesnį lankstumą ir lengvesnę priežiūrą, ypač dirbant su sudėtingais tipų ryšiais.
- Naudokite tipo apribojimus apgalvotai: Neperkraukite tipo parametrų apribojimais. Siekkite kuo bendresnių tipų, kurie vis dar užtikrina būtiną tipų saugumą.
- Apsvarstykite poveikį našumui: Pernelyg didelis generinių tipų naudojimas kartais gali paveikti našumą. Profiluokite savo kodą, kad nustatytumėte bet kokias kliūtis.
- Dokumentuokite savo kodą: Aiškiai dokumentuokite savo generinių tipų ir tipo apribojimų paskirtį. Tai padės jūsų kodą lengviau suprasti ir prižiūrėti.
- Testuokite kruopščiai: Rašykite išsamius vienetinius testus (unit tests), kad užtikrintumėte, jog jūsų generinis kodas veikia kaip tikėtasi su skirtingais tipais.
Išvada
TypeScript variantiškumo anotacijų (netiesiogiai per funkcijų parametrų taisykles) ir tipo parametrų apribojimų įvaldymas yra būtinas norint kurti tvirtą, lankstų ir lengvai prižiūrimą kodą. Suprasdami kovariantiškumo, kontravariantiškumo ir invariantiškumo koncepcijas bei efektyviai naudodami tipo apribojimus, galite rašyti generinį kodą, kuris yra ir tipų atžvilgiu saugus, ir pakartotinai naudojamas. Šios technikos yra ypač vertingos kuriant programas, kurios turi tvarkyti įvairius duomenų tipus arba prisitaikyti prie skirtingų aplinkų, kaip tai yra įprasta šiandienos globalizuotame programinės įrangos pasaulyje. Laikydamiesi gerųjų praktikų ir kruopščiai testuodami savo kodą, galite atskleisti visą TypeScript tipų sistemos potencialą ir kurti aukštos kokybės programinę įrangą.