乐趣区

关于前端:TypeScript-官方手册翻译计划五对象类型

  • 阐明 :目前网上没有 TypeScript 最新官网文档的中文翻译,所以有了这么一个翻译打算。因为我也是 TypeScript 的初学者,所以无奈保障翻译百分之百精确,若有谬误,欢送评论区指出;
  • 翻译内容 :暂定翻译内容为 TypeScript Handbook,后续有空会补充翻译文档的其它局部;
  • 我的项目地址 :TypeScript-Doc-Zh,如果对你有帮忙,能够点一个 star ~

本章节官网文档地址:Object Types

对象类型

在 JavaScript 中,最根底的分组和传递数据的形式就是应用对象。在 TypeScript 中,咱们则通过对象类型来示意。

正如之前看到的,对象类型能够是匿名的:

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

在下面的例子中,咱们编写的函数承受的对象蕴含了 name 属性(类型必须是 string)和 age 属性(类型必须是 number)。

属性修饰符

对象类型中的每个属性都能够指定一些货色:属性类型、属性是否可选,属性是否可写。

可选属性

大多数时候,咱们会发现自己解决的对象可能有一个属性集。这时候,咱们能够在这些属性的名字前面加上 ? 符号,将它们标记为可选属性。

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

在这个例子中,xPosyPos 都是可选属性。这两个属性咱们能够抉择提供或者不提供,所以下面的 paintShape 调用都是无效的。可选性真正想要表白的其实是,如果设置了该属性,那么它最好有一个特定的类型。

这些属性同样能够拜访 —— 但如果开启了 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
  // ...
}

在 JavaScript 中,即便素来没有设置过某个属性,咱们也仍然能够拜访它 —— 值是 undefined。咱们能够对 undefined 这种状况做非凡的解决。

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

留神,这种为没有指定的值设置默认值的模式很常见,所以 JavaScript 提供了语法层面的反对。

function paintShape({shape, xPos = 0, yPos = 0}: PaintOptions) {console.log("x coordinate at", xPos);
                                 ^^^^ 
                            // (parameter) xPos: number
  console.log("y coordinate at", yPos);
                                 ^^^^ 
                            // (parameter) yPos: number
  // ...
}

这里咱们为 paintShape 的参数应用了解构模式,同时也为 xPosyPos 提供了默认值。当初,xPosyPospaintShape 函数体中就肯定是有值的,且调用该函数的时候这两个参数依然是可选的。

留神,目前没有任何办法能够在解构模式中应用类型注解。这是因为上面的语法在 JavaScript 中有其它的语义

function draw({shape: Shape, xPos: number = 100 /*...*/}) {render(shape);
        ^^^^^^
     // Cannot find name 'shape'. Did you mean 'Shape'?
  render(xPos);
         ^^^^^
    // Cannot find name 'xPos'.
}

在一个对象解构模式中,shape: Shape 示意“捕捉 shape 属性并将其从新定义为一个名为 Shape 的局部变量”。同理,xPos: number 也会创立一个名为 number 的变量,它的值就是参数中 xPos 的值。

应用映射修饰符能够移除可选属性。

只读属性

在 TypeScript 中,咱们能够将属性标记为 readonly,示意这是一个只读属性。尽管这不会扭转运行时的任何行为,但标记为 readonly 的属性在类型查看期间无奈再被重写。

interface SomeType {readonly prop: string;}
 
function doSomething(obj: SomeType) {
  // 能够读取 obj.prop
  console.log(`prop has the value '${obj.prop}'.`);
 
  // 但无奈从新给它赋值
  obj.prop = "hello";
// Cannot assign to 'prop' because it is a read-only property.
}

应用 readonly 修饰符并不一定意味着某个值是齐全不可批改的 —— 或者换句话说,并不意味着它的内容是不可批改的。readonly 仅示意属性自身不可被重写。

interface Home {readonly resident: { name: string; age: number};
}
 
function visitForBirthday(home: Home) {
  // 咱们能够读取并更新 home.resident 属性
  console.log(`Happy birthday ${home.resident.name}!`);
  home.resident.age++;
}
 
function evict(home: Home) {
  // 但咱们无奈重写 Home 类型的 resident 属性自身
  home.resident = {
       ^^^^^^^^
// Cannot assign to 'resident' because it is a read-only property.
    name: "Victor the Evictor",
    age: 42,
  };
}

了解 readonly 的含意十分重要。在应用 TypeScript 进行开发的过程中,它能够无效地表明一个对象应该如何被应用。TypeScript 在查看两个类型是否兼容的时候,并不会思考它们的属性是否是只读的,所以只读属性也能够通过别名进行批改。

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;
 
console.log(readonlyPerson.age); // 打印 42
writablePerson.age++;
console.log(readonlyPerson.age); // 打印 43

应用映射修饰符能够移除只读属性。

索引签名

有时候你无奈提前晓得某个类型所有属性的名字,但你晓得这些属性值的类型。在这种状况下,你能够应用索引签名去形容可能值的类型。举个例子:

interface StringArray {[index: number]: string
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
      ^^^^^^^^^^    
     // const secondItem: string

下面的代码中,StringArray 接口有一个索引签名。这个索引签名表明当 StringArraynumber 类型的值索引的时候,它将会返回 string 类型的值。

一个索引签名的属性类型要么是 string,要么是 number

当然,也能够同时反对两种类型……

但前提是,数值型索引返回的类型必须是字符串型索引返回的类型的一个子类型。这是因为,当应用数值索引对象属性的时候,JavaScript 实际上会先把数值转化为字符串。这意味着应用 100(数值)进行索引与应用 "100"(字符串)进行索引,成果是一样的,因而这两者必须统一。

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;
// 'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
  [x: string]: Dog;
}

不过,如果索引签名所形容的类型自身是各个属性类型的联结类型,那么就容许呈现不同类型的属性了:

interface NumberOrStringDictionary {[index: string]: number | string;
  length: number; // length 是数字,能够
  name: string; // name 是字符串,能够
}

最初,能够设置索引签名是只读的,这样能够避免对应索引的属性被从新赋值:

interface ReadonlyStringArray {readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// Index signature in type 'ReadonlyStringArray' only permits reading.

因为索引签名设置了只读,所以无奈再更改 myArray[2] 的值。

拓展类型

基于某个类型拓展出一个更具体的类型,这是一个很常见的需要。举个例子,咱们有一个 BasicAddress 类型用于形容邮寄函件或者包裹所须要的地址信息。

interface BasicAddress {
    name?: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}

通常状况下,这些信息曾经足够了,不过,如果某个地址的修建有很多单元的话,那么地址信息通常还须要有一个单元号。这时候,咱们能够用一个 AddressWithUnit 来形容地址信息:

interface AddressWithUnit {
    name?: string;
    unit: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}

这当然没问题,但毛病就在于:尽管只是单纯增加了一个域,但咱们却不得不反复编写 BasicAddress 中的所有域。那么无妨改用一种办法,咱们拓展原有的 BasicAddress 类型,并且增加上 AddressWithUnit 独有的新的域。

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
 
interface AddressWithUnit extends BasicAddress {unit: string;}

跟在某个接口前面的 extends 关键字容许咱们高效地复制来自其它命名类型的成员,并且增加上任何咱们想要的新成员。这对于缩小咱们必须编写的类型申明语句有很大的作用,同时也能够表明领有雷同属性的几个不同类型申明之间存在着分割。举个例子,AddressWithUnit 不须要反复编写 street 属性,且因为 street 属性来自于 BasicAddress,开发者能够晓得这两个类型之间存在着某种分割。

接口也能够拓展自多个类型:

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;}
type ColorfulCircle = Colorful & Circle;

这里,咱们联合 ColorfulCircle 类型,产生了一个新的类型,它领有 ColorfulCircle 的所有成员。

function draw(circle: Colorful & Circle) {console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
 
// 能够运行
draw({color: "blue", radius: 42});
 
// 不能运行
draw({color: "red", raidus: 42});
/*
Argument of type '{color: string; raidus: number;}' is not assignable to parameter of type 'Colorful & Circle'.
  Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
*/ 

接口 VS 穿插类型

目前,咱们理解到了能够通过两种形式去联合两个类似但存在差别的类型。应用接口,咱们能够通过 extends 子句拓展原有类型;应用穿插类型,咱们也能够实现相似的成果,并且应用类型别名去命名新类型。这两者的本质区别在于它们解决抵触的形式,而这个区别通常就是咱们在接口和穿插类型的类型别名之间抉择其一的次要理由。

泛型对象类型

假如咱们有一个 Box 类型,它可能蕴含任何类型的值:stringnumberGiraffe 等。

interface Box {contents: any;}

当初,contents 属性的类型是 any,这当然没问题,但应用 any 可能会导致类型平安问题。

因而咱们能够改用 unknown。但这意味着只有咱们晓得了 contents 的类型,咱们就须要做一个预防性的查看,或者应用容易出错的类型断言。

interface Box {contents: unknown;}
 
let x: Box = {contents: "hello world",};
 
// 咱们能够查看 x.contents
if (typeof x.contents === "string") {console.log(x.contents.toLowerCase());
}
 
// 或者应用类型断言
console.log((x.contents as string).toLowerCase());

还有另一种确保类型平安的做法是,针对每种不同类型的 contents,创立不同的 Box 类型。

interface NumberBox {contents: number;}
 
interface StringBox {contents: string;}
 
interface BooleanBox {contents: boolean;}

但这意味着咱们须要创立不同的函数,或者是函数的重载,这样能力操作不同的 Box 类型。

function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any}, newContents: any) {box.contents = newContents;}

这会带来十分多的样板代码。而且,咱们后续还可能引入新的类型和重载,这未免有些冗余,毕竟咱们的 Box 类型和重载只是类型不同,本质上是一样的。

无妨改用一种形式,就是让 Box 类型申明一个类型参数并应用泛型。

interface Box<Type> {contents: Type;}

你能够将这段代码解读为“Box 的类型是 Type,它的 contents 的类型是 Type”。接着,当咱们援用 Box 的时候,咱们须要传递一个类型参数用于代替 Type

let box: Box<string>;

如果把 Box 看作是理论类型的模板,那么 Type 就是一个会被其它类型代替的占位符。当 TypeScript 看到 Box<string> 的时候,它会将 Box<Type> 中的所有 Type 替换为 string,失去一个相似 {contents: string} 的对象。换句话说,Box<string> 和之前例子中的 StringBox 是等效的。

interface Box<Type> {contents: Type;}
interface StringBox {contents: string;}
 
let boxA: Box<string> = {contents: "hello"};
boxA.contents;
     ^^^^^^^^   
    // (property) Box<string>.contents: string
 
let boxB: StringBox = {contents: "world"};
boxB.contents;
     ^^^^^^^^   
    // (property) StringBox.contents: string

因为 Type 能够被任何类型替换,所以 Box 具备可重用性。这意味着当咱们的 contents 须要一个新类型的时候,齐全无需再申明一个新的 Box 类型(尽管这么做没有任何问题)。

interface Box<Type> {contents: Type;}
interface Apple {//...}
// 和 {contents: Apple} 一样
type AppleBox = Box<Apple>;

这也意味着,通过应用泛型函数,咱们能够完全避免应用重载。

function setContents<Type>(box: Box<Type>, newContents: Type) {box.contents = newContents;}

值得注意的是,类型别名也能够应用泛型。之前定义的 Box<Type> 接口:

interface Box<Type> {contents: Type;}

能够改写为上面的类型别名:

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

类型别名和接口不一样,它不仅仅能够用于形容对象类型。所以咱们也能够应用类型别名编写其它的泛型工具类型。

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
     ^^^^^^^^^^^^^^
    //type OneOrManyOrNull<Type> = OneOrMany<Type> | null
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
     ^^^^^^^^^^^^^^^^^^^^^^          
    // type OneOrManyOrNullStrings = OneOrMany<string> | null         

咱们稍后会再绕回来解说类型别名。

数组类型

泛型对象类型通常是某种容器类型,独立于它们所蕴含的成员的类型工作。数据结构以这种形式工作十分现实,即便数据类型不同也能够重复使用。

实际上,在这本手册中,咱们始终都在和一个泛型打交道,那就是 Array(数组)类型。咱们编写的 number[] 类型或者 string[] 类型,实际上都是 Array<number>Array<string> 的简写。

function doSomething(value: Array<string>) {// ...}
 
let myArray: string[] = ["hello", "world"];
 
// 上面两种写法都能够!doSomething(myArray);
doSomething(new Array("hello", "world"));

就和后面的 Box 类型一样,Array 自身也是一个泛型:

interface Array<Type> {
  /**
   * 获取或者设置数组的长度
   */
  length: number;
 
  /**
   * 移除数组最初一个元素,并返回该元素
   */
  pop(): Type | undefined;
 
  /**
   * 向数组增加新元素,并返回数组的新长度
   */
  push(...items: Type[]): number;
 
  // ...
}

古代 JavaScript 也提供了其它同样是泛型的数据结构,比方 Map<K,V>Set<T>Promise<T>。这其实意味着,MapSetPromise 的表现形式使得它们可能解决任意的类型集。

只读数组类型

ReadonlyArray(只读数组)是一种非凡的类型,它形容的是无奈被批改的数组。

function doStuff(values: ReadonlyArray<string>) {
  // 咱们能够读取 values 
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // ... 然而无奈批改 values
  values.push("hello!");
         ^^^^
        // Property 'push' does not exist on type 'readonly string[]'.}

就像属性的 readonly 修饰符一样,它次要是一种用来表明用意的工具。当咱们看到一个函数返回 ReadonlyArray 的时候,意味着咱们不打算批改这个数组;当咱们看到一个函数承受 ReadonlyArray 作为参数的时候,意味着咱们能够传递任何数组给这个函数,而无需放心数组会被批改。

Array 不一样,ReadonlyArray 并没有对应的构造函数能够应用。

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

不过,咱们能够把一般的 Array 赋值给 ReadonlyArray

const roArray: ReadonlyArray<string> = ["red","green","blue"];

TypeScript 不仅为 Array<Type> 提供了简写 Type[],也为 ReadonlyArray<Type> 提供了简写 readonly Type[]

function doStuff(values: readonly string[]) {
  // 咱们能够读取 values
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // ... 但无奈批改 values
  values.push("hello!");
        ^^^^^
      // Property 'push' does not exist on type 'readonly string[]'.}

最初一件须要留神的事件是,和 readonly 属性修饰符不同,一般的 ArrayReadonlyArray 之间的可赋值性不是双向的。

let x: readonly string[] = [];
let y: string[] = [];
 
x = y;
y = x;
^
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

元组类型

元组类型是一种非凡的 Array 类型,它的元素数量以及每个元素对应的类型都是明确的,

type StringNumberPair = [string, number];

这里,StringNumberPair 是一个蕴含 string 类型和 number 类型的元组类型。和 ReadonlyArray 一样,它没有对应的运行时示意,但对于 TypeScript 仍十分重要。对于类型零碎而言,StringNumberPair 形容了这样的一个数组:下标为 0 的地位蕴含了一个 string 类型的值,下标为 1 的地位蕴含了一个 number 类型的值。

function doSomething(pair: [string, number]) {const a = pair[0];
        ^
     //const a: string
  const b = pair[1];
        ^        
     // const b: number
  // ...
}
 
doSomething(["hello", 42]);

如果拜访元组元素的时候下标越界,那么会抛出一个谬误。

function doSomething(pair: [string, number]) {
  // ...
 
  const c = pair[2];
                ^    
// Tuple type '[string, number]' of length '2' has no element at index '2'.
}

咱们也能够应用 JavaScript 的数组解构去解构元组。

function doSomething(stringHash: [string, number]) {const [inputString, hash] = stringHash;
 
  console.log(inputString);
              ^^^^^^^^^^^    
        // const inputString: string
 
  console.log(hash);
              ^^^^   
            // const hash: number
}

元组类型在高度基于约定的 API 中很有用,因为每个元素的含意都是“明确的”。这给予了咱们一种灵活性,让咱们在解构元组的时候能够给变量取任意名字。在下面的例子中,咱们能够给下标为 0 和 1 的元素取任何名字。

不过,怎么才算“明确”呢?每个开发者的见解都不一样。兴许你须要重新考虑一下,在 API 中应用带有形容属性的对象是否会更好。

除了长度查看之外,相似这样的简略元组类型其实等价于一个对象,这个对象申明了特定下标的属性,且蕴含了数值字面量类型的 length 属性。

interface StringNumberPair {
  // 特定的属性
  length: 2;
  0: string;
  1: number;
 
  // 其它 Array<string | number> 类型的成员
  slice(start?: number, end?: number): Array<string | number>;
}

另一件你可能感兴趣的事件是,元组类型也能够领有可选元素,只须要在某个元素类型前面加上 ?。可选的元组元素只能呈现在最初面,并且会影响该类型的长度。

type Either2dOr3d = [number, number, number?];
 
function setCoordinate(coord: Either2dOr3d) {const [x, y, z] = coord;
               ^
            // const z: number | undefined
 
  console.log(`Provided coordinates had ${coord.length} dimensions`);
                                             ^^^^^^                                
                                        // (property) length: 2 | 3
}

元组也能够应用开展运算符,运算符前面必须跟着数组或者元组。

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
  • StringNumberBooleans 示意这样的一个元组:它的前两个元素别离是 stringnumber 类型,同时前面跟着若干个 boolean 类型的元素。
  • StringBooleansNumber 示意这样的一个元组:它的第一个元素是 string 类型,接着跟着若干个 boolean 类型的元素,最初一个元素是 number
  • BooleansStringNumber 示意这样的一个元组:它后面有若干个 boolean 类型的元素,最初两个元素别离是 stringnumber 类型。

应用开展运算符的元组没有明确的长度 —— 能够明确的只是它的不同地位有对应类型的元素。

const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];

为什么可选元素和开展运算符很有用呢?因为它容许 TypeScript 将参数列表对应到元组上。在残余参数和开展运算符中能够应用元组,所以上面这段代码:

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

和这段代码是等效的:

function readButtonInput(name: string, version: number, ...input: boolean[]) {// ...}

有时候,咱们应用残余参数承受数量可变的若干个参数,同时又要求参数不少于某个数量,且不想为此引入两头变量,这时候下面的这种写法就十分不便了。

只读元组类型

对于元组类型还有最初一点须要留神的,那就是 —— 元组类型也能够是只读的,通过在元组后面加上 readonly 修饰符,咱们能够申明一个只读的元组类型 —— 就像只读数组的简写一样。

function doSomething(pair: readonly [string, number]) {// ...}

在 TypeScript 中无奈重写只读元组的任何属性。

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

在大部分的代码中,元组被创立后就不须要批改了,所以将元组注解为只读类型是一个不错的默认做法。还有一点很重要的是,应用 const 断言的数组字面量将会被推断为只读元组。

let point = [3, 4] as const;
 
function distanceFromOrigin([x, y]: [number, number]) {return Math.sqrt(x ** 2 + y ** 2);
}
 
distanceFromOrigin(point);
                 ^^^^^^     
/* 
Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
  The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'. 
*/

这里,distanceFromOrigin 没有批改元组的元素,但它冀望承受一个可变的元组。因为 point 的类型被推断为 readonly [3,4],所以它和 [number, number] 是不兼容的,因为后者无奈保障 point 的元素不会被批改。

退出移动版