13

In TypeScript code, I often will see code wrapped inside of Angle Brackets, just like HTML. I know that they are not HTML Elements, and I know that the code inside of the angle brackets are types, however; I see types written without angle-brackets all the time. It seems that there is a very specific & fundamental purpose for wrapping types inside of the angle brackets, and I feel that much of what I fail to understand can be deduced from the answer to this question.


I would like to know why the angle brackets are part of the TypeScript language, what the angle brackets do programmatically, and what effect do the angle brackets have on the code inside of them.


For example: What is the purpose of adding the angle brackets here? How should I interpret them?

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


JΛYDΞV
  • 8,532
  • 3
  • 51
  • 77
Michael
  • 1,313
  • 11
  • 25
  • 3
    The other question is not similar to this one. It asks about a different use of the angle brackets (type assertions). This question is about [generics](https://www.typescriptlang.org/docs/handbook/generics.html). They are explained in the documentation. – axiac Sep 29 '19 at 08:07

3 Answers3

43

When you learn Typescript, you actually learn not one language, but two. The first language is the Typescript proper, which is Javascript with type annotations and some extensions, like "enum" or "public/private" class members. The second language is the language of types. It has no official name, let's call it Anders after the inventor, Anders Hejlsberg.

The purpose of Anders is to generate dynamic types for your program. While Typescript manipulates values that are strings, numbers, objects etc, Anders only deals with a single kind of data: the type itself. Anders' values are types. A function in Anders accepts one or multiple type arguments and returns another type.

Every time you use <> in your program, you actually write Anders code, not Typescript code. This code can be called either explicitly (when you write something like MyType<T>), or under the hood, via type inference.

For example, here's a Typescript function, which accepts two values and returns another value, based on them:

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

This is an Anders function, which accepts two types and returns another type, based on them:

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

In Typescript, if you give pair two values, you'll get an array of these two values.

In Anders, if you give Pair number (not any number, the "number" type), and string, you'll get back [number, string], which is the type of all possible number,string arrays, like [1, "hi"] or [3.14, "hey"]. If you give it string and boolean, you'll get the type of all arrays like ["hi", true], ["blah", false].

Like other languages, Anders provides basic programming constructs (that, to recap, all are types or act on types, not values):

  • built-in types, like number, string, any, {}. These are similar to Typescript built-in objects like "Number" or "String".

  • literals, like "foo". These are similar to literals in Typescript, but while in TS "foo" means a specific string, e.g. a sequence of characters f, o, o, in Anders it means a type, namely, "the type of all strings that are foo", which, obviously, has only one possible member, "foo".

  • unions, similar to arrays in TS: A|B|C.

  • structures, similar to objects in TS. In TS, an object maps strings to values. In Anders, a structure (aka "mapped type"), maps types to other types. The index operator S[B] returns the type to which the structure S maps B

        {foo: string; bar:number}["foo"]` ====> string
    
  • operators, e.g. the unary keyof operator takes a type A and returns the type of all possible keys of A, that is, a union (array) TypeOfKey1 | TypeOfKey2 | ...

        keyof {foo:string, bar:number} =====> "foo"|"bar"
    
  • comparisons, like a > b in TS. Anders only has one form of comparison, A extends B, which means that A is a subset of B, that is, all possible values of the type A are also values of B, but not necessarily the other way around.

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

  • loops, like {[A in SomeUnion]: T}. This creates a structure, whose keys are the union members and values are of type T

        {[A in "foo"|"bar"]: number} =====> {foo:number, bar:number}
    
  • function calls, which are SomeOtherTypeDeclaration<Type1, Type2, ...>

  • finally, Anders also have type checks for input parameters, similar to function foo(x:number) in Typescript. In Anders, a type check is a comparison, that is, A extends B

Now, back to your example (simplified for clarity).

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 is the Anders function, which accepts a type K and returns another type (X, Y) => Z, which is a type of all functions that have two arguments of types X and Y and the return value is of type Z.

Let's "call" this function manually with different types and see what happens.

  1. getContent<number>. First off, Anders checks the type for the argument. Our type check is extends keyof ContentMap. As we recall, keyof ContentMap returns an array of keys of ContentMap, that is "foo"|"bar" where, again, "foo" and "bar" are types and not just strings. Then, our argument, number, is checked against "foo"|"bar". Obviously, number is not a subset of this type, so the type check fails and we get an error.

  2. getContent<"foo">. The type check succeeds (since "foo" is a subset of "foo"|"bar") and we can proceed. Our task is to construct the function type based on "foo". The first param has the type K, the same as the argument, so it becomes just "foo". The second param applies the index operator twice: first, we evaluate ContentMap["foo"], which gives us {conf: A, content: B} and then we apply ["conf"], which gives us A. In the similar way, we obtain B for the return type. Finally, we call the built-in Anders function Readonly and get back another type, let's call it ReadonlyB, So, what we've got is the function type (content: "foo", conf: A) => ReadonlyB, and this is what our Anders function returns.

  3. getContent<"bar"> ... left as an exercise.

Now, what happens when you write this?

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

The compiler sees that you have some Anders code, related to getContent and evaluates that code, passing "foo" as an argument. As seen above, the return type will be ("foo", A) => ReadonlyB. Then, the above line is checked against this type, and fails if it doesn't match, which is basically what the whole thing is all about.

Hope this helps...

georg
  • 211,518
  • 52
  • 313
  • 390
  • Man thats simply amazing! Where you learn these stuff? I googled and checked many blogs but no one explained this clear. You should write a book and I will definitely buy one! – Michael Sep 30 '19 at 06:45
19

As @axiac mentioned, it is to do with generics.

The way you can read it is to think of type.

Example:

// 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
Soc
  • 7,425
  • 4
  • 13
  • 30
  • 3
    This is so much easier to understand than that super long accepted answer, thank you! – Timo Ernst Jul 19 '20 at 10:37
  • 1
    @TimoErnst Ya, well as nice as a short answer is, you should appriciate the long answer more, as everything in it is important to understanding this question. This answer is short and sweet, and for someone who knows nothing about the topic, this answer is fine, but eventually everyone who becomes proficient at TS will need to know what the long answer provides. I am always grateful for long answers. – JΛYDΞV Jun 19 '22 at 12:44
0

Angled brackets use in Typescript is called Generics. Here is an excellent video on Generics.

https://www.youtube.com/watch?v=IOzkOXSz9gE

  • 1
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/32306219) – Quentin Jul 25 '22 at 10:57