Skip to content

Commit d2c55f5

Browse files
committed
index types, indexed access types and mapped types. closed #150. closed #156
1 parent 89d3a07 commit d2c55f5

File tree

1 file changed

+232
-10
lines changed

1 file changed

+232
-10
lines changed

doc/handbook/Advanced Types.md

+232-10
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ pet.swim(); // errors
125125

126126
# 类型保护与区分类型(Type Guards and Differentiating Types)
127127

128-
联合类型非常适合这样的情形,可接收的值有不同的类型
129-
当我们想明确地知道是否拿到`Fish`时会怎么做
130-
JavaScript里常用来区分2个可能值的方法是检查它们是否存在
131-
像之前提到的,我们只能访问联合类型的所有类型中共有的成员
128+
联合类型适合于那些值可以为不同类型的情况
129+
但当我们想确切地了解是否为`Fish`时怎么办
130+
JavaScript里常用来区分2个可能值的方法是检查成员是否存在
131+
如之前提及的,我们只能访问联合类型中共同拥有的成员
132132

133133
```ts
134134
let pet = getSmallPet();
@@ -157,21 +157,21 @@ else {
157157

158158
## 用户自定义的类型保护
159159

160-
可以注意到我们使用了多次类型断言
161-
如果我们只要检查过一次类型,就能够在后面的每个分支里清楚`pet`的类型的话就好了。
160+
可以注意到我们不得不多次使用类型断言
161+
假如一旦我们检查过类型,就能够在之后的每个分支里清楚的知道`pet`的类型的话就好了。
162162

163163
TypeScript里的*类型保护*机制让它成为了现实。
164164
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
165-
要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个*类型断言*
165+
要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个*类型谓词*
166166

167167
```ts
168168
function isFish(pet: Fish | Bird): pet is Fish {
169169
return (<Fish>pet).swim !== undefined;
170170
}
171171
```
172172

173-
在这个例子里,`pet is Fish`就是类型断言
174-
一个断言是`parameterName is Type`这种形式,`parameterName`必须是来自于当前函数签名里的一个参数名。
173+
在这个例子里,`pet is Fish`就是类型谓词
174+
一个谓词为`parameterName is Type`这种形式,`parameterName`必须是来自于当前函数签名里的一个参数名。
175175

176176
每当使用一些变量调用`isFish`时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
177177

@@ -305,7 +305,6 @@ sn = undefined; // error, 'undefined'不能赋值给'string | null'
305305
```
306306

307307
注意,按照JavaScript的语义,TypeScript会把`null``undefined`区别对待。
308-
309308
`string | null``string | undefined``string | undefined | null`是不同的类型。
310309

311310
## 可选参数和可选属性
@@ -671,3 +670,226 @@ let v = new ScientificCalculator(2)
671670
如果没有`this`类型,`ScientificCalculator`就不能够在继承`BasicCalculator`的同时还保持接口的连贯性。
672671
`multiply`将会返回`BasicCalculator`,它并没有`sin`方法。
673672
然而,使用`this`类型,`multiply`会返回`this`,在这里就是`ScientificCalculator`
673+
674+
# 索引类型(Index types)
675+
676+
使用索引类型,就可以让编译器检查使用了动态属性名代码。
677+
例如,常见的JavaScript模式是从对象中选取属性的子集。
678+
679+
```js
680+
function pluck(o, names) {
681+
return names.map(n => o[n]);
682+
}
683+
```
684+
685+
下面是如何在TypeScript里使用此函数,使用**索引类型查询****索引访问**操作符:
686+
687+
```ts
688+
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
689+
return names.map(n => o[n]);
690+
}
691+
692+
interface Person {
693+
name: string;
694+
age: number;
695+
}
696+
let person: Person;
697+
let strings: string[] = pluck(person, ['name']); // ok, string[]
698+
```
699+
700+
编译器会检查`name`是否为`Person`的属性,且它清楚`strings``string[]`类型,因为`name``string`类型。
701+
为了让它工作,这个例子引入了几个类型操作符。
702+
首先是`keyof T`**索引类型查询操作符**
703+
对于任何类型`T``keyof T`的结果为`T`上已知的公共属性名的联合。
704+
例如:
705+
706+
```ts
707+
let personProps: keyof Person; // 'name' | 'age'
708+
```
709+
710+
`keyof Person`是完全可以与`'name' | 'age'`互相替换。
711+
不同的是如果你添加其它的属性到`Person`,假设是`address: string`,那么`keyof Person`会自动变成`'name' | 'age' | 'address'`
712+
你可以在像在`pluck`这样的普通上下文里使用`keyof`,你在使用之前并不清楚可能出现的属性名。
713+
就是说编译器会检查你是否传入了正确的属性名给`pluck`
714+
715+
```ts
716+
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
717+
```
718+
719+
第二个操作符是`T[K]`**索引访问操作符**
720+
这里,类型语法反映了表达式语法。
721+
这意味着`person['name']`具有类型`Person['name']` &mdash; 在我们的例子里则为`string`
722+
然而,就像索引类型查询一样,你可以在普通的上下文里使用`T[K]`,这正是它的强大所在。
723+
你只在确保类型变量`K extends keyof T`
724+
下面是一个使用了函数的例子。
725+
726+
```ts
727+
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
728+
return o[name]; // o[name] is of type T[K]
729+
}
730+
```
731+
732+
`getProperty`里的`o: T``name: K`,意味着`o[name]: T[K]`
733+
当你返回`T[K]`的结果,编译器会实例化键的真实类型,因此`getProperty`的返回值类型会随着你需要的属性改变。
734+
735+
```ts
736+
let name: string = getProperty(person, 'name');
737+
let age: number = getProperty(person, 'age');
738+
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
739+
```
740+
741+
## 索引类型和字符串索引签名
742+
743+
`keyof``T[K]`与字符串索引签名进行交互。
744+
如果你有一个带有字符串索引签名的类型,那么`keyof T`会是`string`
745+
并且`T[string]`为索引签名的类型:
746+
747+
```ts
748+
interface Map<T> {
749+
[key: string]: T;
750+
}
751+
let keys: keyof Map<number>; // string
752+
let value: Map<number>['foo']; // number
753+
```
754+
755+
# 映射类型
756+
757+
一个常见的任务是将一个已知的类型每个属性都变为可选的:
758+
759+
```ts
760+
interface PersonPartial {
761+
name?: string;
762+
age?: number;
763+
}
764+
```
765+
766+
或者我们想要一个只读版本:
767+
768+
```ts
769+
interface PersonReadonly {
770+
readonly name: string;
771+
readonly age: number;
772+
}
773+
```
774+
775+
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 &mdash; **映射类型**
776+
在映射类型里,新类型以相同的形式去转换旧类型里每个属性。
777+
例如,你可以令每个属性成为`readonly`类型或可选的。
778+
下面是一些例子:
779+
780+
```ts
781+
type Readonly<T> = {
782+
readonly [P in keyof T]: T[P];
783+
}
784+
type Partial<T> = {
785+
[P in keyof T]?: T[P];
786+
}
787+
```
788+
789+
像下面这样使用:
790+
791+
```ts
792+
type PersonPartial = Partial<Person>;
793+
type ReadonlyPerson = Readonly<Person>;
794+
```
795+
796+
下面来看看最简单的映射类型和它的组成部分:
797+
798+
```ts
799+
type Keys = 'option1' | 'option2';
800+
type Flags = { [K in Keys]: boolean };
801+
```
802+
803+
它的语法与索引签名的语法类型,内部使用了`for .. in`
804+
具有三个部分:
805+
806+
1. 类型变量`K`,它会依次绑定到每个属性。
807+
2. 字符串字面量联合的`Keys`,它包含了要迭代的属性名的集合。
808+
3. 属性的结果类型。
809+
810+
在个简单的例子里,`Keys`是硬编码的的属性名列表并且属性类型永远是`boolean`,因此这个映射类型等同于:
811+
812+
```ts
813+
type Flags = {
814+
option1: boolean;
815+
option2: boolean;
816+
}
817+
```
818+
819+
在真正的应用里,可能不同于上面的`Readonly``Partial`
820+
它们会基于一些已存在的类型,且按照一定的方式转换字段。
821+
这就是`keyof`和索引访问类型要做的事情:
822+
823+
```ts
824+
type NullablePerson = { [P in keyof Person]: Person[P] | null }
825+
type PartialPerson = { [P in keyof Person]?: Person[P] }
826+
```
827+
828+
但它更有用的地方是可以有一些通用版本。
829+
830+
```ts
831+
type Nullable<T> = { [P in keyof T]: T[P] | null }
832+
type Partial<T> = { [P in keyof T]?: T[P] }
833+
```
834+
835+
在这些例子里,属性列表是`keyof T`且结果类型是`T[P]`的变体。
836+
这是使用通用映射类型的一个好模版。
837+
因为这类转换是[同态](https://en.wikipedia.org/wiki/Homomorphism)的,映射只作用于`T`的属性而没有其它的。
838+
编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。
839+
例如,假设`Person.name`是只读的,那么`Partial<Person>.name`也将是只读的且为可选的。
840+
841+
下面是另一个例子,`T[P]`被包装在`Proxy<T>`类里:
842+
843+
```ts
844+
type Proxy<T> = {
845+
get(): T;
846+
set(value: T): void;
847+
}
848+
type Proxify<T> = {
849+
[P in keyof T]: Proxy<T[P]>;
850+
}
851+
function proxify<T>(o: T): Proxify<T> {
852+
// ... wrap proxies ...
853+
}
854+
let proxyProps = proxify(props);
855+
```
856+
857+
注意`Readonly<T>``Partial<T>`用处不小,因此它们与`Pick``Record`一周被包含进了TypeScript的标准库里:
858+
859+
```ts
860+
type Pick<T, K extends keyof T> = {
861+
[P in K]: T[P];
862+
}
863+
type Record<K extends string | number, T> = {
864+
[P in K]: T;
865+
}
866+
```
867+
868+
`Readonly``Partial``Pick`是同态的,但`Record`不是。
869+
因为`Record`并不需要输入类型来拷贝属性,所以它不属于同态:
870+
871+
```ts
872+
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
873+
```
874+
875+
非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。
876+
877+
## 由映射类型进行推断
878+
879+
现在你了解了如何包装一个类型的属性,那么接下来就是如果拆包。
880+
其实这也非常容易:
881+
882+
```ts
883+
function unproxify<T>(t: Proxify<T>): T {
884+
let result = {} as T;
885+
for (const k in t) {
886+
result[k] = t[k].get();
887+
}
888+
return result;
889+
}
890+
891+
let originalProps = unproxify(proxyProps);
892+
```
893+
894+
注意这个拆包推断只适用于同态的映射类型。
895+
如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

0 commit comments

Comments
 (0)