为 TypeScript Handbook Object Types 一章的阅读笔记
表示对象方式
- anonymous, 内联写法
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
- interface
interface Person {
name: string;
age: number;
}
- type alias
type Person = {
name: string;
age: number;
}
Property Modifiers
Optional Properties
就是在属性名后面加个问号, 表示属性可能是 undefined
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
但是只有 strictNullChecks 开起来后, ts才会检查类型是否为 undefined
function paintShape(opts: PaintOptions) {
// (property) PaintOptions.xPos?: number | undefined
let xPos = opts.xPos;
// (property) PaintOptions.yPos?: number | undefined
let yPos = opts.yPos;
// ...
}
要为某个 optional 属性设置默认值, 可以使用解构
interface Shape {}
declare function getShape(): Shape;
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
// ---cut---
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
// ^?
console.log("y coordinate at", yPos);
// ^?
// ...
}
但是无法在使用解构的时候同时给属性标注类型
// @noImplicitAny: false
// @errors: 2552 2304
interface Shape {}
declare function render(x: unknown);
// ---cut---
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
// Cannot find name 'shape'. Did you mean 'Shape'?(2552)
render(shape);
// Cannot find name 'xPos'.(2304)
render(xPos);
}
因为这个写法是js属性解构取别名的语法, ts不能再改变他的意思了
// orignial
function render() {}
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
render(xPos);
}
// after babel compile
"use strict";
function render() {}
function draw(_ref) {
var Shape = _ref.shape,
_ref$xPos = _ref.xPos,
number = _ref$xPos === void 0 ? 100 : _ref$xPos;
render(shape);
render(xPos);
}
readonly Properties
readonly 表明这个属性不能再次赋值
// @errors: 2540
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
// Cannot assign to 'prop' because it is a read-only property.(2540)
obj.prop = "hello";
}
但是如果他的值是一个对象的话, 里面的内容是可以改变的, 就和const一样
// @errors: 2540
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {
name: "Victor the Evictor",
age: 42,
};
}
ts在比较类型是否兼容的时候不会考虑readonly, 所以下面的操作也是合法的:
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
Mapping Modifiers
我们可以使用+或者-来添加或者移除readonly、?, +是可以省略的
// Removes 'readonly' attributes from a type's properties
type RemoveReadonly<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
// Add 'readonly' attributes to a type's properties
type AddReadonly<Type> = {
+readonly [Property in keyof Type]: Type[Property];
};
// Removes 'optional' attributes from a type's properties
type RemoveOptional<Type> = {
[Property in keyof Type]-?: Type[Property];
};
// Add 'optional' attributes to a type's properties
type AddOptional<Type> = {
[Property in keyof Type]+?: Type[Property];
};
type User = {
readonly id?: string;
readonly name: string;
age: number;
};
type RemoveReadonlyUser = RemoveReadonly<User>;
// type RemoveReadonlyUser = { id?: string | undefined; name: string; age: number; }
type AddReadonlyUser = AddReadonly<User>;
// type AddReadonlyUser = { readonly id?: string | undefined; readonly name: string; readonly age: number; }
type RemoveOptionalUser = RemoveOptional<User>;
// type RemoveOptionalUser = { readonly id: string; readonly name: string; age: number; }
type AddOptionalUser = AddOptional<User>;
// type AddOptionalUser = { readonly id?: string | undefined; readonly name?: string | undefined; age?: number | undefined; }
Index Signatures
当一个对象的 key 无法一一列举, 但是 value 的类型却是可以确定的时候, index signatures 就派上用场了
declare function getStringArray(): StringArray;
// ---cut---
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
// const secondItem: string
ts 4.1 的 compilerOptions 新增了一个 noUncheckedIndexedAccess 的选项, 开启的话, 会给使用 index signatures 的 value 类型加上 undefined
declare function getStringArray(): StringArray;
// ---cut---
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
// const secondItem: string | undefined
Try
上下文参考 release notes
我们知道, 一个对象的key, 可以是 string, number, symbol
type StringAsIndex = {
[key: string]: string
}
const o1: StringAsIndex= {
'key': 'value'
}
type NumberAsIndex = {
[key: string]: string
}
const o2: NumberAsIndex= {
1: 'value'
}
type SymbolAsIndex = {
[key: symbol]: string
}
const o3: SymbolAsIndex= {
[Symbol('whatever')]: 'value'
}
在 js 里, 如果用 number 作为 key 的话, 实际上都会自动转成 string
o = {}
o[1] = 'value'
// true
o[1] === o['1']
Object.keys(o)
// ['1']
ts 在类型上也兼容了这种表现
type StringAsIndex = {
[key: string]: string
}
const o: StringAsIndex= {
// it's ok
1: 'value'
}
那么同时使用 number 和 string 作为 index 的时候呢? [key: number]value的类型必须是 [key: string]value的子类型
// @errors: 2413
// @strictPropertyInitialization: false
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
如果一个对象使用了index signatures来描述, 那么这意味着这个对象上的所有key-value, 都必须遵守这个规则:
// @errors: 2411
// @errors: 2411
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
// Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
declare function getReadOnlyStringArray(): ReadonlyStringArray;
// ---cut---
// @errors: 2542
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
工具类型 Record 就是用 index signature 实现的
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
Combine Types
Interfaces with extends
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
Intersection Types with type alias
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
Interfaces vs. Intersections
既然使用 interface extends 和 intersection types 都能起到复用已有类型的效果, 那么他们的主要区别是什么? key 重复的时候(conflicting), 处理的策略不一样:
interface IA {
a: string
}
interface IA extends IA {
// Subsequent property declarations must have the same type. Property 'a' must be of type 'string', but here has type 'number'.(2717)
a: number
}
type A = {
a: string
}
type B = { a: number } & { a: string }
// number & string == never
type B_a = B['a']
Generic Object Types
interface IBox<Type> {
contents: Type;
}
type Box<Type> = {
contents: Type;
};
Array<T>, Map<K, V>, Set<T>, and Promise<T> 都使用了范型
Array Types
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
// ...
}
T[] 是 Array<T>的简写
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
同时还有只读版本的 ReadonlyArray<T>
// @errors: 2339
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
}
和Array不一样, ReadonlyArray没有构造函数签名
// 'ReadonlyArray' only refers to a type, but is being used as a value here.(2693)
new ReadonlyArray("red", "green", "blue");
类似的, ReadonlyArray<Type> 也可以写成 readonly Type[]
// @errors: 2339
function doStuff(values: readonly string[]) {
// ^^^^^^^^^^^^^^^^^
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
}
值得注意的是, 和上文说到的readonly property modifier不会影响类型兼容不同, 正常的Array可以赋值给ReadonlyArray, 但反过来不行
// @errors: 4104
let x: readonly string[] = [];
let y: string[] = [];
x = y;
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]' (4104)
y = x;
// it is ok
let a: {readonly k: string} = { k: 'v' }
let b: {k: string} = { k: 'v' }
a = b
b = a
Tuple Types
tuple type 就是类型更为具体的 Array type
// @errors: 2493
function doSomething(pair: [string, number]) {
// ...
const a = pair[0];
const b = pair[1];
// Tuple type '[string, number]' of length '2' has no element at index '2'.(2493)
const c = pair[2];
}
具体在哪里? 元素类型可以确定, 长度可以确定, 方法类型可以确定
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}
const tuple:StringNumberPair = ['1', 2]
最后面的元素同样可以使用 ? 表示 optional
// it is ok
type TupleOptional1 = [number, number?];
type TupleOptional2 = [number, number?, number?];
type TupleOptional3 = [number, number?, number?, number?];
// error, A required element cannot follow an optional element.
type TupleOptional4 = [number, number?, number];
当然, 也可以加 readonly
// @errors: 2540
function doSomething(pair: readonly [string, number]) {
// Cannot assign to '0' because it is a read-only property.(2540)
pair[0] = "hello!";
}
tuple 可以包含 rest elements, 当然这个时候 length 就没办法限制啦
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
这一点对描述函数参数类型很有用
function readButtonInput1(name: string, version: number, ...input: boolean[]) {
// ...
}
function readButtonInput2(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}