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.
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.
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.
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...