文章目录
对象类型
属性修饰符
在 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 });
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,可以利用这个进行判断进行未传值的逻辑处理。
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
// ...
}
❗❗❗请注意,目前没有办法在解构模式中放置类型注释。这是因为解构中会将类型理解为解构参数的重命名。
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape); // 报错 Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos); // 报错 Cannot find name 'xPos'.
}
readonly 属性
对于 TypeScript,属性也可以标记为 readonly。虽然它不会在运行时改变任何行为,但在类型检查期间无法写入标记为 readonly 的属性。
使用 readonly 修饰符并不一定意味着值是完全不可变的 - 或者换句话说,其内部内容无法更改。这只是意味着属性本身不能被重写,但如果是对象的话,只是对象本身不能改变,但对象内部的属性可以改变。
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 = {
// 报错 Cannot assign to 'resident' because it is a read-only property.
name: "Victor the Evictor",
age: 42,
};
}
索引签名 [index: number]: string
有时你并不提前知道类型属性的所有名称,但你确实知道值的类型。
在这些情况下,你可以使用索引签名来描述可能值的类型,例如下方示例,它表示有一个 StringArray 接口,它有一个索引签名。这个索引签名表明当一个 StringArray 被一个 number 索引时,它将返回一个 string。
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
const secondItem: string
索引签名属性只允许使用某些类型:string、number、symbol、模板字符串模式以及仅由这些组成的联合类型。
虽然字符串索引签名是描述 “dictionary” 模式的强大方式,但它们还强制所有属性与其返回类型匹配。但是,如果索引签名是属性类型的联合,则可以接受不同类型的属性。
nterface 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";
你不能设置 myArray[2],因为索引签名是 readonly。
溢出属性检查
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
let mySquare = createSquare({ colour: "red", width: 100 });
// 报错 Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'.
// Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
为了应对上面例子中属性名称不匹配类型导致的报错,可以有以下方法避免该报错。
断言 as
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
索引签名 [propName: string]: any
重新定义 SquareConfig 接口,[propName: string]: any 表示 SquareConfig 可以有任意多个数除 color 和 with 的属性,属性名是 string 类型,属性值是 any 类型。
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
重新分配变量
绕过这些检查的最后一种方法(可能有点令人惊讶)是将对象分配给另一个变量:由于分配 squareOptions 不会进行溢出属性检查,因此编译器不会给你错误。
只要你在 squareOptions 和 SquareConfig 之间具有共同属性,上述变通方法就会起作用。在此示例中,它是属性 width。但是,如果变量没有任何公共对象属性,它将失败。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
扩展字段 extends
如果两个对象只有少数属性不同,不用重复去声明,用 extends 扩展基础接口去实现属性扩展。
继承单个接口
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
继承多个接口
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
交叉类型 &
interface 允许我们通过 extends 扩展其他类型来构建新类型。TypeScript 提供了另一种称为交叉类型的构造,主要用于组合现有的对象类型。
交叉类型是使用 & 运算符定义的。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
// 用法一
type ColorfulCircle = Colorful & Circle;
// 用法二
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 }); // 报错 raidus 不匹配
接口与交叉
接口的扩展和交叉类型的类型别名,这两种方式都能实现类似的效果,两者之间的主要区别在于冲突的处理方式,这种区别通常是你在接口和交叉类型的类型别名之间选择一个而不是另一个的主要原因之一。
泛型对象类型
Array 类型
interface Array<Type> {
length: number;
pop(): Type | undefined;
push(...items: Type[]): number;
}
现代 JavaScript 还提供了其他泛型的数据结构,如 Map<K, V>、Set 和 Promise。所有这一切真正意味着由于 Map、Set 和 Promise 的行为方式,它们可以与任何类型的集合一起使用。
ReadonlyArray 类型
ReadonlyArray 是一种特殊类型,用于描述不应更改的数组。
function doStuff(values: ReadonlyArray<string>) {
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
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[] 提供简写语法一样,它也为 ReadonlyArray 和 readonly Type[] 提供简写语法。
function doStuff(values: readonly string[]) {
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
values.push("hello!"); // Property 'push' does not exist on type 'readonly string[]'.
}
可赋值性
最后要注意的一点是,与 readonly 属性修饰符不同,可赋值性在常规 Array 和 ReadonlyArray 之间不是双向的。
常规的数组可以赋值给只读的数组,反过来不可以。
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[]'.
tuple 元组类型
元组类型是另一种 Array 类型,它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型。
如果我们试图索引超过元素的数量,我们会得到一个错误。
function doSomething(pair: [string, number]) {
const a = pair[0];
// const a: string
const b = pair[1];
// const b: number
const c = pair[2]; // 报错 Tuple type '[string, number]' of length '2' has no element at index '2'.
}
doSomething(["hello", 42]);
我们也可以使用 JavaScript 的数组解构来 解构元组。
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
console.log(inputString);
// const inputString: string
console.log(hash);
// const hash: number
}
你可能感兴趣的另一件事是元组可以通过写出问号(元素类型后的 ?)来具有可选属性。可选的元组元素只能放在最后,也会影响 length 的类型。
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 描述了一个元组,其前两个元素分别是 string 和 number,但后面可以有任意数量的 boolean。
StringBooleansNumber 描述一个元组,其第一个元素是 string,然后是任意数量的 boolean,最后以 number 结尾。
BooleansStringNumber 描述了一个元组,其起始元素是任意数量的 boolean,并以 string 和 number 结尾。
具有剩余元素的元组没有设置 “length” - 它只有一组不同位置的众所周知的元素。
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 元组类型
关于元组类型的最后一点说明 - 元组类型有 readonly 变体,可以通过在它们前面添加 readonly 修饰符来指定 - 就像数组简写语法一样。
正如你所料,TypeScript 中不允许写入 readonly 元组的任何属性。
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!"; // Cannot assign to '0' because it is a read-only property.
}
as const 断言
在大多数代码中,元组往往被创建并保持不变,因此尽可能将类型注释为 readonly 元组是一个很好的默认设置。这一点也很重要,因为带有 const 断言的数组字面将使用 readonly 元组类型来推断。
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 的元素不会发生修改。