Types from Extraction
TypeScript's type system is very powerful because it allows expressing types in terms of other types. Although the simplest form of this is generics, we actually have a wide variety of type operators available to us. It's also possible to express types in terms of values that we already have.
By combining various type operators, we can express complex operations and values in a succinct, maintainable way. In this chapter we'll cover ways to express a type in terms of an existing type or value.
The typeof
type operator
JavaScript already has a typeof
operator you can use in an expression context:
// Prints "string" console.log(typeof "Hello world");
TypeScript adds a typeof
operator you can use in a type context to refer to the type of a variable or property:
let s = "hello"; let n: typeof s; ▲let n: string
This isn't very useful for basic types, but combined with other type operators, you can use typeof
to conveniently express many patterns.
For an example, let's start by looking at the predefined type ReturnType<T>
.
It takes a function type and produces its return type:
type Predicate = (x: unknown) => boolean; type K = ReturnType<Predicate>; ▲type K = boolean
If we try to use ReturnType
on a function name, we see an instructive error:
function f() { return { x: 10, y: 3 }; } type P = ReturnType<f'f' refers to a value, but is being used as a type here.>;Try'f' refers to a value, but is being used as a type here.
Remember that values and types aren't the same thing.
To refer to the type that the value f
has, we use typeof
:
function f() { return { x: 10, y: 3 }; } type P = ReturnType<typeof f>; ▲type P = { x: number; y: number; }Try
Limitations
TypeScript intentionally limits the sorts of expressions you can use typeof
on.
Specifically, it's only legal to use typeof
on identifiers (i.e. variable names) or their properties.
This helps avoid the confusing trap of writing code you think is executing, but isn't:
// Meant to use = let x : msgbox(',' expected."Are you sure you want to continue?");',' expected.
The keyof
type operator
The keyof
operator takes a type and produces a string or numeric literal union of its keys:
type Point = { x: number, y: number }; type P = keyof Point; ▲type P = "x" | "y"
If the type has a string
or number
index signature, keyof
will return those types instead:
type Arrayish = { [n: number]: unknown }; type A = keyof Arrayish; ▲type A = number type Mapish = { [k: string]: boolean }; type M = keyof Mapish; ▲type M = string | numberTry
Note that in this example, M
is string | number
-- this is because JavaScript object keys are always coerced to a string, so obj[0]
is always the same as obj["0"]
.
keyof
types become especially useful when combined with mapped types, which we'll learn more about later.
Indexed Access Types
We can use typeof
to reference the type of a property of a value.
What if we want to reference the type of a property of a type instead?
We can use an indexed access type to look up a specific property on another type:
type Person = { age: number, name: string, alive: boolean }; type A = Person["age"]; ▲type A = number
The indexing type is itself a type, so we can use unions, keyof
, or other types entirely:
type I1 = Person["age" | "name"]; ▲type I1 = string | number type I2 = Person[keyof Person]; ▲type I2 = string | number | boolean type AliveOrName = "alive" | "name"; type I3 = Person[AliveOrName]; ▲type I3 = string | booleanTry
You'll even see an error if you try to index a property that doesn't exist:
type I1 = Person["alve"Property 'alve' does not exist on type 'Person'.];Property 'alve' does not exist on type 'Person'.
Another example of indexing with an arbitrary type is using number
to get the type of an array's elements.
We can combine this with typeof
to conveniently capture the element type of an array literal:
const MyArray = [ { name: "Alice", age: 15 }, { name: "Bob", age: 23 }, { name: "Eve", age: 38 } ]; type T = (typeof MyArray)[number]; ▲type T = { name: string; age: number; }Try