TypeScript Object Types

219 阅读3分钟

为 TypeScript Handbook Object Types 一章的阅读笔记

表示对象方式

  1. anonymous, 内联写法
function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}
  1. interface
interface Person {
  name: string;
  age: number;
}
  1. 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 });

Try

但是只有 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;
  // ...
}

Try

要为某个 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);
  //                             ^?
  // ...
}

Try

但是无法在使用解构的时候同时给属性标注类型

// @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);
}

Try

因为这个写法是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);
}

Try

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";
}

Try

但是如果他的值是一个对象的话, 里面的内容是可以改变的, 就和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,
  };
}

Try

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; }

Try

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

Try

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'
}

Try

在 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'
}

Try

那么同时使用 numberstring 作为 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;
}

Try

如果一个对象使用了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";

Try

工具类型 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,
};

Try

Intersection Types with type alias

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

type ColorfulCircle = Colorful & Circle;

Try

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']

Try

Generic Object Types

interface IBox<Type> {
  contents: Type;
}

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

Try

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;

  // ...
}

Try

T[]Array<T>的简写

function doSomething(value: Array<string>) {
  // ...
}

let myArray: string[] = ["hello", "world"];

// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));

Try

同时还有只读版本的 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!");
}

Try

Array不一样, ReadonlyArray没有构造函数签名

// 'ReadonlyArray' only refers to a type, but is being used as a value here.(2693)
new ReadonlyArray("red", "green", "blue");

Try

类似的, 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!");
}

Try

值得注意的是, 和上文说到的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

Try

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];
}

Try

具体在哪里? 元素类型可以确定, 长度可以确定, 方法类型可以确定

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]

Try

最后面的元素同样可以使用 ? 表示 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];

Try

当然, 也可以加 readonly

// @errors: 2540
function doSomething(pair: readonly [string, number]) {
  // Cannot assign to '0' because it is a read-only property.(2540)
  pair[0] = "hello!";
}

Try

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];

Try

这一点对描述函数参数类型很有用

function readButtonInput1(name: string, version: number, ...input: boolean[]) {
  // ...
}
function readButtonInput2(...args: [string, number, ...boolean[]]) {
  const [name, version, ...input] = args;
  // ...
}

Try