typer - Vad betyder vinkelparenteser „<>" i funktionsdeklaration i Typescript?


I TypeScript-kod ser jag ofta kod omsluten av vinkelparenteser, precis som i HTML. Jag vet att de inte är HTML-element, och jag vet att koden inuti vinkelparenteserna är typer, men jag ser typer skrivna utan vinkelparenteser hela tiden. Det verkar som att det finns ett mycket specifikt och grundläggande syfte med att omsluta typer inuti vinkelparenteser, och jag känner att mycket av det jag inte förstår kan utläsas från svaret på denna fråga.

Jag skulle vilja veta varför vinkelparenteserna är en del av TypeScript-språket, vad vinkelparenteserna gör programmatiskt, och vilken effekt vinkelparenteserna har på koden inuti dem.

Till exempel: Vad är syftet med att lägga till vinkelparenteserna här? Hur ska jag tolka dem?

getContent<K extends keyof ContentMap>(content: K, conf?: ContentMap[K]["conf"]): Promise<Readonly<ContentMap[K]["content"]>>;

Svar #1

När du lär dig Typescript, lär du dig faktiskt inte ett språk, utan två. Det första språket är själva Typescript, som är Javascript med typannoteringar och några tillägg, som "enum" eller "public/private" klassmedlemmar. Det andra språket är typers språk. Det har inget officiellt namn, vi kan kalla det Anders efter uppfinnaren, Anders Hejlsberg.

Syftet med Anders är att generera dynamiska typer för ditt program. Medan Typescript manipulerar värden som är strängar, nummer, objekt etc., hanterar Anders endast en typ av data: typen själv. Anders värden är typer. En funktion i Anders accepterar ett eller flera typargument och returnerar en annan typ.

Varje gång du använder <> i ditt program, skriver du faktiskt Anders kod, inte Typescript-kod. Denna kod kan anropas antingen explicit (när du skriver något som MyType<T>), eller under huven, via typutledning.

Till exempel, här är en Typescript-funktion som tar emot två värden och returnerar ett annat värde baserat på dem:

function pair (x, y) {
    return [x, y]
}

Detta är en Anders-funktion, som accepterar två typer och returnerar en annan typ, baserat på dem:

type Pair<U, V> = [U, V]

I Typescript, om du ger pair två värden, kommer du att få en array av dessa två värden.

I Anders, om du ger Pair number (inte vilket nummer som helst, utan typen "number"), och string, får du tillbaka [number, string], vilket är typen av alla möjliga number,string-arrayer, som [1, "hi"] eller [3.14, "hey"]. Om du ger den string och boolean, får du typen av alla arrayer som ["hi", true], ["blah", false].

Liksom andra språk tillhandahåller Anders grundläggande programmeringskonstruktioner (som, för att sammanfatta, alla är typer eller verkar på typer, inte på värden):

  • inbyggda typer, som number, string, any, {}. Dessa är liknande Typescript inbyggda objekt som "Number" eller "String".

  • literals, som "foo". Dessa är liknande literals i Typescript, men medan "foo" i TS betyder en specifik sträng, t.ex. en sekvens av tecken f, o, o, betyder det i Anders en typ, nämligen "typen av alla strängar som är foo", vilket, uppenbarligen, bara har ett möjligt medlem, "foo".

  • unioner, liknande arrayer i TS: A|B|C.

  • strukturer, liknande objekt i TS. I TS mappar ett objekt strängar till värden. I Anders mappar en struktur (även kallad "mappad typ"), typer till andra typer. Indexoperatorn S[B] returnerar typen som strukturen S mappar B till

        {foo: string; bar:number}["foo"]` ====> string
    
  • operatorer, t.ex. tar den unära keyof-operatorn en typ A och returnerar typen av alla möjliga nycklar i A, det vill säga en union (array) TypeOfKey1 | TypeOfKey2 | ...

        keyof {foo:string, bar:number} =====> "foo"|"bar"
    
  • jämförelser, som a > b i TS. Anders har bara en form av jämförelse, A extends B, vilket betyder att A är en delmängd av B, det vill säga att alla möjliga värden av typen A också är värden av B, men inte nödvändigtvis tvärtom.

        "foo" extends string =====> ok
        "foo" extends "foo"|"bar" =====> ok
        "blag" extends "foo"|"bar" =====> not ok
    
  • villkor: comparison ? Type1 : Type2

  • loopar, som {[A in SomeUnion]: T}. Detta skapar en struktur, vars nycklar är medlemskap i unionen och värden är av typen T

        {[A in "foo"|"bar"]: number} =====> {foo:number, bar:number}
    
  • funktionsanrop, som är SomeOtherTypeDeclaration<Type1, Type2, ...>

  • slutligen har Anders också typkontroller för inparametrar, liknande function foo(x:number) i Typescript. I Anders är en typkontroll en jämförelse, det vill säga A extends B

Nu, tillbaka till ditt exempel (förenklat för tydlighetens skull).

interface A {}
interface B {}
interface C {}
interface D {}

type ContentMap = {
  foo: {
      conf: A
      content: B
  },
  bar: {
      conf: C
      content: D
  }
}

function getContent<K extends keyof ContentMap>
  ( content: K,
    conf?: ContentMap[K]["conf"]
  ): Readonly<ContentMap[K]["content"]> {
      ...
  }

getContent är Anders funktion, som accepterar en typ K och returnerar en annan typ (X, Y) => Z, vilket är en typ av alla funktioner som har två argument av typerna X och Y och returvärdet är av typen Z.

Låt oss "anropa" den här funktionen manuellt med olika typer och se vad som händer.

  1. getContent<number>. Först kontrollerar Anders typen för argumentet. Vår typkontroll är extends keyof ContentMap. Som vi minns, returnerar keyof ContentMap en array av nycklar i ContentMap, det vill säga "foo"|"bar" där, återigen, är "foo" och "bar" typer och inte bara strängar. Sedan kontrolleras vårt argument, number, mot "foo"|"bar". Oenbart är number inte en delmängd av denna typ, så typkontrollen misslyckas och vi får ett fel.

  2. getContent<"foo">. Typkontrollen lyckas (eftersom "foo" är en delmängd av "foo"|"bar") och vi kan fortsätta. Vår uppgift är att konstruera funktionstypen baserat på "foo". Den första parametern har typen K, samma som argumentet, så den blir helt enkelt "foo". Den andra parametern tillämpar indexoperatorn två gånger: först utvärderar vi ContentMap["foo"], vilket ger oss {conf: A, content: B} och sedan tillämpar vi ["conf"], vilket ger oss A. På liknande sätt får vi B för returtypen. Slutligen anropar vi den inbyggda Anders-funktionen Readonly och får tillbaka en annan typ, låt oss kalla den ReadonlyB. Så, det vi har fått är funktionstypen (content: "foo", conf: A) => ReadonlyB, och detta är vad vår Anders-funktion returnerar.

  3. getContent<"bar"> ... lämnas som en övning.

Nu, vad händer när du skriver detta?

let something = getContent('foo', {...})

Kompilatorn ser att du har viss Anders-kod, relaterad till getContent och utvärderar den koden, och skickar "foo" som argument. Som ses ovan kommer returtypen att vara ("foo", A) => ReadonlyB. Sedan kontrolleras raden ovan mot denna typ, och misslyckas om den inte matchar, vilket i princip är vad hela grejen handlar om.

Hoppas det här hjälper...

Svar #2

Som @axiac nämnde, har det att göra med generics.

Sättet du kan läsa det på är att tänka på typ.

Exempel :

// generic class that deals with type T
class List<T> {}

// usage
const list1 = new List<string>() // list of type string
const list2 = new List<number>() // list of type number
const list3 = new List<any>()    // list of type any

Relaterade artiklar