U TypeScriptu, što znači <T>?


Razmotrite:

export declare function createEntityAdapter<T>(options?: {
    selectId?: IdSelector<T>;
    sortComparer?: false | Comparer<T>;
}): EntityAdapter<T>;

Što znači <T>? Znam da <> predstavlja tvrdnju o tipu, ali ne znam što je 'T'. Što radi ova funkcija?

Odgovor #1

Što znači <T>?

To je deklaracija generika u TypeScriptu.

Izvadak :

Veliki dio softverskog inženjeringa je izrada komponenti koje ne samo da imaju jasno definirane i dosljedne API-je, već su i ponovno upotrebljive. Komponente koje su sposobne raditi s današnjim podacima, kao i s podacima sutrašnjice, pružit će vam najsvestranije mogućnosti za izgradnju velikih softverskih sustava.

U jezicima poput C# i Java, jedan od glavnih alata u okviru za stvaranje ponovno upotrebljivih komponenti su generici, to jest, mogućnost stvaranja komponente koja može raditi s različitim tipovima umjesto s jednim. To korisnicima omogućuje da koriste te komponente i koriste vlastite tipove.

Spomenuli ste:

Ne znam što je 'T'.

'T' će biti tip deklariran u vrijeme izvođenja, umjesto u vrijeme kompajliranja. Varijabla T može biti bilo koja nedeklarirana varijabla (nisam mogao pronaći referencu, ali pretpostavio bih da se radi o bilo kojem važećem skupu znakova koji se mogu koristiti za imena varijabli). Slično, u C#, ako tip koji T predstavlja nije vrijednosni tip, već složeniji tip (klasa) ili sučelje, mogao bi se imenovati/deklarirati kao TVehicle ili TAnimal kako bi se pomoglo budućim programerima u prepoznavanju važećeg tipa (i moglo bi se smatrati najboljom praksom jer samo T nije intuitivno). Ja preferiram TSomething jer znam da veliko slovo T označava generički tip. WSomething ili ASomething također su valjani, ali ih jednostavno ne preferiram. (Microsoftove API-je gotovo uvijek koriste, na primjer, TContext ili TEntity).

Također bi bilo korisno ako mi netko može objasniti što ova funkcija radi.

Pa, funkcija zapravo ne radi ništa. Ovo je više deklaracija tipa funkcije koja može imati više vrijednosti tipa u vrijeme izvođenja. Umjesto da to objašnjavam, uključit ću odlomak preuzet direktno s gornje poveznice.

function identity<T>(arg: T): T {
  return arg;
}

koje se može koristiti ovako:

// type of output will be 'string'
let output = identity<string>("myString");

ili

// type of output will be 'string', the compiler will figure out `T`
// based on the value passed in
let output = identity("myString");

ili

// type of output will be 'number'
let output = identity(8675309);

Što bi moglo postaviti pitanje:

Zašto koristiti generike

JavaScript ima nizove, ali kada dohvatite vrijednost iz niza, ona doslovno može biti bilo što (TypeScript: any). S TypeScriptom dobivate sigurnost tipa tako da ih deklarirate ovako:

 // Array<T>
 let list: number[] = [1, 2, 3];
 // or
 let list: Array<number> = [1, 2, 3];

Sada svaki vrijednost u nizu ima tip. TypeScript će baciti pogrešku u vrijeme kompajliranja ako pokušate staviti string u ovaj niz. I dobivate sigurnost tipa i intellisense (ovisno o vašem uređivaču) kada dohvatite vrijednost:

class Person {
  FirstName: string;
}

let people: Array<Person> = [];
people.push({ FirstName: "John" } as Person);

let john = people.pop();
// john is of type Person, the TypeScript compiler knows this
// because we've declared the people variable as an array of Person

console.log(john.FirstName);

Deklariranje generičkih ograničenja tipa. Vrlo dobar primjer Principa otvorenosti-zatvorenosti.

U objektno-orijentiranom programiranju, princip otvorenosti/zatvorenosti navodi "softverske jedinice (klase, moduli, funkcije itd.) trebale bi biti otvorene za proširenje, ali zatvorene za izmjenu"; tj. takva jedinica može omogućiti proširenje svog ponašanja bez izmjene svog izvornog koda.

U sljedećem primjeru, bilo tko bi mogao proširiti Human ili Cheetah ili čak stvoriti vlastitu izvedenu vrstu i funkcionalnost Loggera bi i dalje radila bez ikakvih izmjena.

interface IAnimal {
  LegCount: number;
}

class Cheetah
  implements IAnimal {
  LegCount: number = 4;
}

class Human
  implements IAnimal {
  LegCount: number = 2;
}

public class Logger<TAnimal extends IAnimal> {
  public Log(animal: TAnimal) {
    console.log(animal.LegCount);
  }
}

var logger = new Logger();
var human = new Human();
logger.Log(human);

Radni primjer

U prethodnom primjeru koristio sam generičko ograničenje kako bih ograničio tip TAnimal koji programeri mogu koristiti za stvaranje instance Logger na tipove koji izvedu sučelje IAnimal. To omogućuje prevoditelju da pretpostavi da tip vrijednosti proslijeđen klasi Logger uvijek ima svojstvo LegCount.

Možeš mi objasniti zašto u TypeScript dokumentaciji stavljaju <T> umjesto da stave nešto opisnije poput <Identity> na primjer. Ništa i <T> mi je isto. Sad, svi koriste <T> kao budale, ili sam nešto propustio?

Sve će ovo biti pretpostavke u nastavku. Ne poznajem ni tim koji je dizajnirao TypeScript generički sustav ni tim koji je napisao dokumentaciju.

Na osnovnoj razini generika je sposobnost korištenja T kao bilo kojeg mogućeg tipa (što se ne smije miješati s TypeScriptovim any). To znači da je Array<T> sučelje (zbog nedostatka boljeg izraza) i kada kreiramo konkretni tip, zamjenjujemo T s deklariranim tipom:

Array<number>

Dakle, za sučelje Array<T> što ima više smisla od T? Ne znam. Znam da T mora biti Tip (broj, string itd.), tako da ima smisla koristiti T jer je to prvo slovo riječi Type. Mislim da bi Array<Type> bilo stvarno zbunjujuće i/ili možda čak nevažeće ako bi type ili Type postali rezervirani ili ograničeni (trenutno type ima posebno značenje u određenim kontekstima, pa je i to loš odabir), stoga izbjegavanje tih riječi je dobar izbor. Drugi jezici (C-sharp, Java) također biraju koristiti T, pa je prednost u promjeni između jezika i mogućnosti korištenja istog pojma.

S druge strane, što bi sljedeće značilo?

Array<Identity>

Što je Identity ovdje? Nema ograničenja koja bi pomogla drugim programerima ili budućim programerima da znaju što to jest. Čini mi se da je to specifično tipizirani niz koji moram eksplicitno implementirati, što znači da nije na meni da biram generički tip.

interface Foo1 {
  bars: Array<Identity>;
}

U prethodnom primjeru, ja (i vjerojatno većina programera) pretpostavio bih da je Identity postojeći tip i ne mogu ga mijenjati.

interface Foo2<T> {
  bars: Array<T>;
}

S Foo2 znam da moram odabrati tip.

interface Foo3<Identity> {
  bars: Array<Identity>;
}

Foo3 je samo zbunjujuće.

interface Foo4<TIdentity> {
  bars: Array<TIdentity>;
}

Sada s Foo4, puno sam sigurniji da moram odabrati tip, ali još uvijek sam malo zbunjen zašto TIdentity. Očito, u nekim kontekstima, gdje je tip jasnije definiran, to bi imalo smisla.

Od ožujka 2021., dokumentacija za TypeScript ažurirana je kako bi se obeshrabrila upotreba <T> u dokumentaciji, u korist <Type>.

S obzirom na to, ja osobno smatram sljedeće zbunjujućim i toplo bih preporučio izbjegavanje Type iz dva razloga.

interface Lengthwise {
  length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}

Prvi razlog za izbjegavanje imenovanja vašeg tipa Type kada koristite ograničenja je taj što se ne čita baš dobro unutar metode niti kod više metoda. Zamislite da su metode za zapisivanje u log mnogo duže od 2 retka, i odjednom čitate samo riječ Type posvuda.

interface Lengthwise {
  length: number;
}

interface Widthwise {
  width: number;
}

function loggingLength<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}

function loggingWidth<Type extends Widthwise >(arg: Type): Type {
  console.log(arg.width);
  return arg;
}

Mislim da je sljedeće mnogo čitljivije:

interface Lengthwise {
  length: number;
}

interface Widthwise {
  width: number;
}

function loggingLength<TLengthwise extends Lengthwise>(arg: TLengthwise): TLengthwise {
  console.log(arg.length);
  return arg;
}

function loggingWidth<TWidthwise extends Widthwise>(arg: TWidthwise ): TWidthwise {
  console.log(arg.width);
  return arg;
}

Drugo, vrlo je moguće imati više od jednog generika, što čini sljedeće nevažećim (nadamo se iz očitih razloga).

interface Lengthwise {
  length: number;
}

interface Widthwise {
  width: number;
}

function loggingLength<Type extends Lengthwise, Type extends Widthwise>
(arg1: Type, arg2: Type) {
  console.log(arg1.length);
  console.log(arg2.width);
}

Iako dokumentacija koristi Type za prikazivanje jednostavnih i običnih primjera, toplo preporučujem da Type ne koristite u stvarnom kodu.

Odgovor #2

Vrijedi spomenuti da je od ožujka 2021. dokumentacija za TypeScript ažurirana kako bi se ukinula upotreba <T> u dokumentaciji, u korist <Type>. Link.

Odgovor #3

Primjer koji pružate je funkcija s generičkim parametrom. T (koji ne mora biti T. Mogli biste ga nazvati G.) naziva se generičkom predloškom gdje se stvarna vrsta T zamjenjuje u runtime-u.

Zamislite da EntityAdapter ima sljedeću implementaciju:

interface EntityAdapter<T> {
   save(entity: T);
}

Donji kod vraća EntityAdapter čiji je sadržaj any. any može biti broj, string, objekt ili što god.

let adapter1 = createEntityAdapter<any>(<parameters here>)

Donji kod vraća EntityAdapter čiji je sadržaj Car.

let adapter2 = createEntityAdapter<Car>(<parameters here>)

U osnovi Car je specifičniji od any tako da možete dobiti dodatnu sigurnost tipa. Pa kako ovo pomaže?

Ukratko, generički predložak može vam dati dodatnu sigurnost tipa. Na primjer,

adapter1.save('I am string') // this works because `T` is `any`
adapter1.save(new Car()) // this also works because `T` is `any`

adapter2.save('I am string') // this wont works  because `T` is `Car`, typescript compiler will complains
adapter2.save(new Car()) //this works because `T` is `Car`

Nadam se da ovo pomaže.

Odgovor #4

To se zove Generics, pomaže nam pisati sigurne, ali fleksibilne funkcije. Omogućuje varijablama da imaju tipove. Ukratko, možete stvoriti funkciju, sučelje ili klasu koja radi s različitim tipovima, bez da unaprijed specificirate tip. Ispod je primjer koda koji objašnjava zašto ga koristimo i kako ga koristiti.

// any is too general ,and it loses the meaning of using typescript

function insertAtBegining(array:any[],value:any) {
    let newArr = [value, ...array];
    return newArr;
}

// if we specify the type number , this function will lost it's utility 

function insertAtBegining(array:number[],value:number) {
    let newArr = [value, ...array];
    return newArr;
}

// if we specify the type string , this function will lost it's utility 

function insertAtBegining(array:string[],value:string) {
    let newArr = [value, ...array];
    return newArr;
}

const demoArr = [1, 2, 3];

const updatedArr = insertAtBegining(demoArr, 4);
// more generic functions

function insertAtBegining<T>(array:T[],value:T) {
    let newArr = [value, ...array];
    return newArr;
}

Povezani članci