TypeScript 对象类型

对象类型

在TypeScript中,我们通过对象类型(object types)来描述对象。 对象类型的名称可以是匿名的:

function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}

也可以使用接口进行定义:

interface Person {
  name: string;
  age: number;
}
 
function greet(person: Person) {
  return "Hello " + person.name;
}

或者通过类型别名定义:

type Person = {
  name: string;
  age: number;
};
 
function greet(person: Person) {
  return "Hello " + person.name;
}

属性修饰符

可选属性

我们可以在属性名后面加一个?标记这个属性是可选的:

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模式下,TypeScript会提示我们,属性值可能是undefined

function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos;              
  // (property) PaintOptions.xPos?: number | undefined
  let yPos = opts.yPos;
  // (property) PaintOptions.yPos?: number | undefined
}

readonly属性

在TypeScript中,属性可以被标记为readonly,这不会改变任何运行时的行为,但在类型检查的时候,一个被标记为readonly的属性是不能被写入的。 个人理解为,标记为readonly的属性,只是储存在栈空间中的值或者引用地址不能变,也就是不能被重新赋值。

interface Home {
  readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
  console.log(`Happy birthday ${home.resident.name}!`);
  // 改变引用类型内部的属性是可以的
  home.resident.age++;
}
 
function evict(home: Home) {
  // 重新赋值会报错
  home.resident = {
    name: "Victor the Evictor",
    age: 42,
  };
}

还有一种情况就是,TypeScript在检查两个类型是否兼容的时候,不会考虑两个类型里的属性是否readonly,这就意味着,readonly的值是可以通过别名修改的。

interface Person {
  name: string;
  age: number;
}
 
interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}
 
let writablePerson: Person = {
  name: "Person McPersonface",
  age: 42,
};
 

let readonlyPerson: ReadonlyPerson = writablePerson;
// 这里由于是将writablePerson的地址赋值给了readonlyPerson
console.log(readonlyPerson.age); // prints '42'
// 所以这里修改writablePerson 必然会影响到readonlyPerson
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'

索引签名

有时候我们并不能提前知道一个类型里所有属性的名字,但是我们知道这些值的特征,这种情况下就可以使用一个索引签名来描述可能的值的类型:

// 这个索引签名表示当一个StringArray类型的值使用number类型的值进行索引时,返回的一定是string类型的数据。
interface StringArray {
  [index: number]: string;
}
 
const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; // const secondItem: string

注意:索引签名参数类型必须是 stringnumbersymbol或模板文本类型。 数字索引的返回类型一定要是字符索引返回类型的子类型。这是因为当使用一个数字进行索引的时候,JavaScript 实际上把它转成了一个字符串。这就意味着使用数字 100 进行索引跟使用字符串 100 索引,是一样的。

interface Animal {
  name: string;
}
 
interface Dog extends Animal {
  breed: string;
}
 
// Error: 用数字字符串索引可能会得到一个完全独立的Animal!
interface NotOkay {
  [x: number]: Animal;
  // “number”索引类型“Animal”不能赋值给“string”索引类型“Dog”  
  [x: string]: Dog;
}

索引签名还会强制要求所有属性都要匹配索引签名的返回类型:

interface NumberDictionary {
  [index: string]: number;
 
  length: number; // ok
  name: string;
	// 类型“string”的属性“name”不能赋给“string”索引类型“number”。
}

不过如果索引签名是联合类型就可以接受了:

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}

当然,索引签名也可以被设置为readonly

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// 类型“ReadonlyStringArray”中的索引签名仅允许读取。

属性继承

在TypeScript中可以使用extends关键字继承接口(interface):

interface Colorful {
  color: string;
}
 
interface Circle {
  radius: number;
}

// 可以继承多个类型 使用,分隔
interface ColorfulCircle extends Colorful, Circle {}
 
const cc: ColorfulCircle = {
  color: "red",
  radius: 42,
};

交叉类型

在TypeScript中可以使用&符号,用于合并已存在的对象类型:

interface Colorful {
  color: string;
}
 
interface Circle {
  radius: number;
}

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
 
// okay
draw({ color: "blue", radius: 42 });

接口继承与交叉类型

这两种方式在合并类型上看起来很相似,但实际上还是有很大不同。最原则性的不同就是在于冲突怎么处理,这也是你决定选择哪种方式的主要原因。

interface Colorful {
  color: string;
}

interface ColorfulSub extends Colorful { 
  color: number
}
// 报错
// 接口“ColorfulSub”错误扩展接口“Colorful”。
// 属性“color”的类型不兼容。
// 不能将类型“number”分配给类型“string”。

使用继承的方式,如果重写类型会导致编译错误,但是交叉类型不会:

interface Colorful {
  color: string;
}

type ColorfulSub = Colorful & {
  color: number
}
// 虽然不会报错,但color最终的类型取得是string和number的交集。也就是never

泛型对象类型

在实际开发中,我们对象类型内的属性类型不一定会预先全部都知道,或者说我们想自由度更高,亦或者是所有类型都适用于这个对象内的属性,那我们就可以使用泛型。

interface Box<T> {
  content: T,
}

let box: Box<string>;

Box想象成一个实际类型的模板,Type就是一个占位符,可以被替代为具体的类型。当TypeScript看到Box<string>,它就会替换为Box<Type>Typestring,最后的结果就会变成{ content: string } 这样做的好处是我们可以复用这个接口,需要不同类型的时候,只需要传入不同的类型去替换Type就好。 类型别名也可以使用泛型:

type Box<Type> = {
  contents: Type;
};

let box: Box<number>;

类型别名不同于接口,可以描述的不只是对象类型,所以可以使用类型别名写一些其他种类的泛型帮助类型。

Array类型

在描述数组类型时,经常使用的是简写方法number[]或者string[],其实他们只是Array<number>Array<string>的简写形式而已。类似于上面举过的Box类型,Array本身就是一个泛型对象类型:

interface Array<T> {
  length: number;
  
  pop(): T | undefined;

  push(...items: T[]): number;
}

现代 JavaScript 也提供其他是泛型的数据结构,比如 Map<K, V>Set<T>Promise<T>。因为 MapSetPromise的行为表现,它们可以跟任何类型搭配使用。

ReadonlyArray类型

ReadonlyArray用来描述一个数组不能被改变:

function doStuff(values: ReadonlyArray<string>) {
  // 我们只可以读取values
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // 但不能更改数组values.
  values.push("hello!");
  // 类型“readonly string[]”上不存在属性“push”。
}

ReadonlyArray 主要是用来做意图声明。当我们看到一个函数返回 ReadonlyArray,就是在告诉我们不能去更改其中的内容,当我们看到一个函数支持传入 ReadonlyArray ,这是在告诉我们我们可以放心的传入数组到函数中,而不用担心会改变数组的内容。 同样的,ReadonlyArray<Type>也有简写方法readonly Type[]

function fn(arr: readonly string[]) {
  console.log(arr[0]);
  arr.push('hello');// 报错:类型“readonly string[]”上不存在属性“push”
}

注意:ArrayReadonlyArray并不能双向赋值:

let x: readonly string[] = [];
let y: string[] = [];
 
x = y; // ok
y = x; // 类型 "readonly string[]" 为 "readonly",不能分配给可变类型 "string[]"。

我个人理解的是,因为readonly描述的是数组内部的项是不能更改的,所以当y赋值给x时,此时x是只读数组,但其实xy此时栈中储存的地址指向的是同一块内存空间,此时如果改变数组yx也会相应被更改,而上面y = x会报错我想是基于纯粹的类型冲突。

let x: readonly string[] = [];
let y: string[] = [];
console.log('x:', x); // x: []
console.log('y:', y); // y: []
x = y; // ok
y.push('1');
console.log('x:', x); // x: [ '1' ]
console.log('y:', y); // y: [ '1' ]