Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.

Commit 076f66e

Browse files
authored
Merge pull request #443 from Microsoft/indexed-types
Document index types, indexed access types and mapped types
2 parents 3b7f2f9 + df5f848 commit 076f66e

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

pages/Advanced Types.md

+213
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,216 @@ let v = new ScientificCalculator(2)
568568
Without `this` types, `ScientificCalculator` would not have been able to extend `BasicCalculator` and keep the fluent interface.
569569
`multiply` would have returned `BasicCalculator`, which doesn't have the `sin` method.
570570
However, with `this` types, `multiply` returns `this`, which is `ScientificCalculator` here.
571+
572+
# Index types
573+
574+
With index types, you can get the compiler to check code that uses dynamic property names.
575+
For example, a common Javascript pattern is to pick a subset of properties from an object:
576+
577+
```js
578+
function pluck(o, names) {
579+
return names.map(n => o[n]);
580+
}
581+
```
582+
583+
Here's how you would write and use this function in TypeScript, using the **index type query** and **indexed access** operators:
584+
585+
```ts
586+
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
587+
return names.map(n => o[n]);
588+
}
589+
590+
interface Person {
591+
name: string;
592+
age: number;
593+
}
594+
let person: Person;
595+
let strings: string[] = pluck(person, ['name']); // ok, string[]
596+
```
597+
598+
The compiler checks that `name` is actually a property on `Person`, and it knows that `strings` is a `string[]` because `name` is a `string`.
599+
To make this work, the example introduces a couple of new type operators.
600+
First is `keyof T`, the **index type query operator**.
601+
For any type `T`, `keyof T` is the union of known, public property names of `T`.
602+
For example:
603+
604+
```ts
605+
let personProps: keyof Person; // 'name' | 'age'
606+
```
607+
608+
`keyof Person` is completely interchangeable with `'name' | 'age'`.
609+
The difference is that if you add another property to `Person`, say `address: string`, then `keyof Person` will automatically update to be `'name' | 'age' | 'address'`.
610+
And you can use `keyof` in generic contexts like `pluck`, where you can't possibly know the property names ahead of time.
611+
That means the compiler will check that you pass the right set of property names to `pluck`:
612+
613+
```ts
614+
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
615+
```
616+
617+
The second operator is `T[K]`, the **indexed access operator**.
618+
Here, the type syntax reflects the expression syntax.
619+
That means that `person['name']` has the type `Person['name']` &mdash; which in our example is just `string`.
620+
However, just like index type queries, you can use `T[K]` in a generic context, which is where its real power comes to life.
621+
You just have to make sure that the type variable `K extends keyof T`.
622+
Here's another example with a function named `getProperty`.
623+
624+
```ts
625+
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
626+
return o[name]; // o[name] is of type T[K]
627+
}
628+
```
629+
630+
In `getProperty`, `o: T` and `name: K`, so that means `o[name]: T[K]`.
631+
Once you return the T[K] result, the compiler will instantiate the actual type of the key, so the return type of `getProperty` will vary according to which property you request.
632+
633+
```ts
634+
let name: string = getProperty(person, 'name');
635+
let age: number = getProperty(person, 'age');
636+
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
637+
```
638+
639+
## Index types and string index signatures
640+
641+
`keyof` and `T[K]` interact with string index signatures.
642+
If you have a type with a string index signature, `keyof T` will just be `string`.
643+
And `T[string]` is just the type of the index signature:
644+
645+
```ts
646+
interface Map<T> {
647+
[key: string]: T;
648+
}
649+
let keys: keyof Map<number>; // string
650+
let value: Map<number>['foo']; // number
651+
```
652+
653+
# Mapped types
654+
655+
A common task is to take an existing type and make each of its properties optional:
656+
657+
```ts
658+
interface PersonPartial {
659+
name?: string;
660+
age?: number;
661+
}
662+
```
663+
664+
Or we might want a readonly version:
665+
666+
```ts
667+
interface PersonReadonly {
668+
readonly name: string;
669+
readonly age: number;
670+
}
671+
```
672+
673+
This happens often enough in Javascript that TypeScript provides a way to create new types based on old types &mdash; **mapped types**.
674+
In a mapped type, the new type transforms each property in the old type in the same way.
675+
For example, you can make all properties of a type `readonly` or optional.
676+
Here are a couple of examples:
677+
678+
```ts
679+
type Readonly<T> = {
680+
readonly [P in keyof T]: T[P];
681+
}
682+
type Partial<T> = {
683+
[P in keyof T]?: T[P];
684+
}
685+
```
686+
687+
And to use it:
688+
689+
```ts
690+
type PersonPartial = Partial<Person>;
691+
type ReadonlyPerson = Readonly<Person>;
692+
```
693+
694+
Let's take a look at the simplest mapped type and its parts:
695+
696+
```ts
697+
type Keys = 'option1' | 'option2';
698+
type Flags = { [K in Keys]: boolean };
699+
```
700+
701+
The syntax resembles the syntax for index signatures with a `for .. in` inside.
702+
There are three parts:
703+
704+
1. The type variable `K`, which gets bound to each property in turn.
705+
2. The string literal union `Keys`, which contains the names of properties to iterate over.
706+
3. The resulting type of the property.
707+
708+
In this simple example, `Keys` is a hard-coded list of property names and the property type is always `boolean`, so this mapped type is equivalent to writing:
709+
710+
```ts
711+
type Flags = {
712+
option1: boolean;
713+
option2: boolean;
714+
}
715+
```
716+
717+
Real applications, however, look like `Readonly` or `Partial` above.
718+
They're based on some existing type, and they transform the fields in some way.
719+
That's where `keyof` and indexed access types come in:
720+
721+
```ts
722+
type NullablePerson = { [P in keyof Person]: Person[P] | null }
723+
type PartialPerson = { [P in keyof Person]?: Person[P] }
724+
```
725+
726+
But it's more useful to have a general version.
727+
728+
```ts
729+
type Nullable<T> = { [P in keyof T]: T[P] | null }
730+
type Partial<T> = { [P in keyof T]?: T[P] }
731+
```
732+
733+
In these examples, the properties list is `keyof T` and the resulting type is some variant of `T[P]`.
734+
This is a good template for any general use of mapped types.
735+
Here's one more example, in which `T[P]` is wrapped in a `Proxy<T>` class:
736+
737+
```ts
738+
type Proxy<T> = {
739+
get(): T;
740+
set(value: T): void;
741+
}
742+
type Proxify<T> = {
743+
[P in keyof T]: Proxy<T[P]>;
744+
}
745+
function proxify<T>(o: T): Proxify<T> {
746+
// ... wrap proxies ...
747+
}
748+
let proxyProps = proxify(props);
749+
```
750+
751+
Note that `Readonly<T>` and `Partial<T>` are so useful, they are included in TypeScript's standard libarary along with `Pick` and `Record`:
752+
753+
```ts
754+
type Pick<T, K extends keyof T> = {
755+
[P in K]: T[P];
756+
}
757+
type Record<K extends string | number, T> = {
758+
[P in K]: T;
759+
}
760+
```
761+
762+
## Inference from mapped types
763+
764+
Now that you know how to wrap the properties of a type, the next thing you'll want to do is unwrap them.
765+
Fortunately, that's pretty easy:
766+
767+
```ts
768+
function unproxify<T>(t: Proxify<T>): T {
769+
let result = {} as T;
770+
for (const k in t) {
771+
result[k] = t[k].get();
772+
}
773+
return result;
774+
}
775+
776+
let originalProps = unproxify(proxyProps);
777+
```
778+
779+
Note that this unwrapping inference works best on *homomorphic* mapped types.
780+
Homomorphic mapped types are mapped types that iterate over every property of some type, and only those properties: `{ [P in keyof T]: X }`.
781+
In the examples above, `Nullable` and `Partial` are homomorphic whereas `Pick` and `Record` are not.
782+
One clue is that `Pick` and `Record` both take a union of property names in addition to a source type, which they use instead of `keyof T`.
783+
If the mapped type is not homomorphic you might have to explicitly give a type parameter to your unwrapping function.

0 commit comments

Comments
 (0)