ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TypeScript 对象类型完全指南:属性修饰符、多余属性检查与泛型容器类型详解

TypeScript 对象类型完全指南:属性修饰符、多余属性检查与泛型容器类型详解 文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载对象是 JavaScript 中最基本的组织和传递数据的方式而 TypeScript 的对象类型则负责在静态层面精确描述它们的形状、可选性、可变性与索引规则。本文以 TypeScript 使用手册中文版的「对象类型」一章为主体系统讲解属性修饰符、多余属性检查、类型扩展与交叉、泛型对象类型以及Array、ReadonlyArray和元组这三大内置容器类型读完即可在真实项目中写出更严谨、可维护的类型声明。对象类型三种声明方式在 TypeScript 中我们可以用三种方式表达一个对象类型。它们可以完全匿名地内联在函数签名中function greet(person: { name: string; age: number }) { return Hello person.name; }也可以通过interface命名interface Person { name: string; age: number; } function greet(person: Person) { return Hello person.name; }还可以通过type类型别名命名type Person { name: string; age: number; }; function greet(person: Person) { return Hello person.name; }三种写法都表示接受一个包含name: string与age: number的对象。匿名写法适合一次性内联使用而interface与type让类型可以被复用、被引用这也是日常开发中最常见的两种命名方式。属性修饰符对象类型中的每个属性都可以指定三方面的信息属性值的类型、属性是否可选以及属性是否可写。可选属性很多场景下对象可能带有某些属性也可能没有。此时可以在属性名末尾加问号?将其标记为可选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 会提示其类型可能是undefinedfunction paintShape(opts: PaintOptions) { let xPos opts.xPos; // 类型为 number | undefined let yPos opts.yPos; // 类型为 number | undefined // ... }这符合 JavaScript 的运行时语义属性从未被设置时访问到的是undefined所以需要显式处理function paintShape(opts: PaintOptions) { let xPos opts.xPos undefined ? 0 : opts.xPos; let yPos opts.yPos undefined ? 0 : opts.yPos; // ... }给未提供的属性设置默认值是如此常见以至于 JavaScript 原生提供了解构默认值语法。结合函数参数解构可以写得更简洁function paintShape({ shape, xPos 0, yPos 0 }: PaintOptions) { console.log(x 坐标为, xPos); // 类型为 number console.log(y 坐标为, yPos); // 类型为 number // ... }此时在函数体内xPos、yPos一定存在有默认值兜底但对调用者来说它们依然可选。注意解构模式中无法放置类型注解。因为{ shape: Shape, xPos: number 100 }在 JavaScript 里已有别的含义——shape: Shape表示取出shape属性并重命名为局部变量ShapexPos: number表示取出xPos属性的值赋给名为number的变量。对象的类型注解应放在解构语法之后而不是解构内部。只读属性用readonly标记的属性在类型检查期间不允许被写入运行时行为不受影响// errors: 2540 interface SomeType { readonly prop: string; } function doSomething(obj: SomeType) { console.log(prop 的值为 ${obj.prop}。); // 可以读取 obj.prop hello; // 错误无法写入只读属性 }readonly只表示属性本身不能被重新赋值并不表示值是深层不可变的——对象内部内容仍然可以修改// errors: 2540 interface Home { readonly resident: { name: string; age: number }; } function visitForBirthday(home: Home) { console.log(生日快乐${home.resident.name}); home.resident.age; // 可行修改的是对象内部的属性 } function evict(home: Home) { home.resident { // 错误无法给 resident 属性本身重新赋值 name: Victor the Evictor, age: 42, }; }还有一点容易踩坑类型兼容性检查不会区分属性是否为readonly。这意味着通过类型别名readonly属性也可以被绕过修改interface Person { name: string; age: number; } interface ReadonlyPerson { readonly name: string; readonly age: number; } let writablePerson: Person { name: Person McPersonface, age: 42 }; // 可行ReadonlyPerson 和 Person 在结构上兼容 let readonlyPerson: ReadonlyPerson writablePerson; console.log(readonlyPerson.age); // 输出 42 writablePerson.age; console.log(readonlyPerson.age); // 输出 43readonlyPerson 看到了变化所以在开发期readonly是表达意图、明确使用方式的工具而非运行期或强制的不可变保证。如果确实需要把属性从readonly变回可写可以使用映射类型的映射修饰符——在映射过程中通过-readonly前缀移除只读特性参见该文映射修饰符一节。索引签名有时我们事先不知道对象所有属性的名字但知道值的大致类型。此时可以用索引签名描述可能的值类型interface StringArray { [index: number]: string; } const myArray: StringArray getStringArray(); const secondItem myArray[1]; // 类型为 string上面的索引签名表示用number索引StringArray时返回string。索引签名的键只允许这些类型string、number、symbol、模板字符串模式以及仅由这些类型组成的联合类型。数字索引与字符串索引可以共存但数字索引的返回类型必须是字符串索引返回类型的子类型。原因在于 JavaScript 对象语义用100索引与用100索引是同一回事数字会被转成字符串所以两者必须保持一致// errors: 2413 interface Animal { name: string; } interface Dog extends Animal { breed: string; } // 错误用数字字符串索引可能得到一个完全不同类型的 Animal interface NotOkay { [x: number]: Animal; [x: string]: Dog; }字符串索引签名是描述字典模式的强大工具但它同时要求所有具名属性与索引返回类型匹配。因为obj.property本质上等价于obj[property]name的类型若与索引类型不一致就会报错// errors: 2411 interface NumberDictionary { [index: string]: number; length: number; // 可行 name: string; // 错误string 不能赋给 number }如果索引签名声明为联合类型则不同属性的不同类型都能被接受interface NumberOrStringDictionary { [index: string]: number | string; length: number; // 可行 name: string; // 可行 }最后索引签名也可以声明为readonly禁止对索引项赋值// errors: 2542 interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray getReadOnlyStringArray(); myArray[2] Mallory; // 错误索引签名是只读的多余属性检查对象被赋予类型的位置和方式会影响类型系统的行为最典型的例子就是多余属性检查excess property checking当对象字面量在创建时直接赋值给某个类型或作为实参传入TypeScript 会进行比普通结构兼容性更彻底的验证。// errors: 2345 2739 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 }); // 错误这里colour拼写错误普通 JavaScript 会静默接受并让 bug 潜伏下来而 TypeScript 认为对象字面量带有目标类型不具备的属性colour很可能是 bug于是报错。注意只有对象字面量在赋值或传参时会触发此检查变量赋值不会。有三种绕过方式但各有适用场景类型断言——最简单let mySquare createSquare({ width: 100, opacity: 0.5 } as SquareConfig);添加字符串索引签名——当你确定对象确实可以携带任意额外属性时更诚实的做法是把它写进类型里interface SquareConfig { color?: string; width?: number; [propName: string]: any; // 允许任意数量的其他属性 }先赋值给中间变量——变量赋值不触发多余属性检查let squareOptions { colour: red, width: 100 }; let mySquare createSquare(squareOptions); // 可行但第三个技巧有前提变量与目标类型之间必须存在公共属性本例是width。如果变量没有任何公共对象属性检查依然会失败// errors: 2559 let squareOptions { colour: red }; let mySquare createSquare(squareOptions); // 错误最后提醒对简单的代码最好不要刻意绕过这些检查。绝大多数多余属性错误实际上是真实的 bug只有处理带方法和状态的复杂对象字面量时才需要考虑上述技巧。如果你在选项包option bag场景频繁触发多余属性错误说明类型声明本身可能需要修正——例如你确实希望createSquare同时接受color或colour就应该把SquareConfig的定义改过来而不是绕过检查。拓展类型interface extends类型系统中经常存在更具体的类型版本。比如一个描述美国寄信地址的BasicAddressinterface BasicAddress { name?: string; street: string; city: string; country: string; postalCode: string; }如果要表达带单元号的地址逐字段复制所有字段显然既啰嗦又容易遗漏还会割裂两个类型之间的关联interface AddressWithUnit extends BasicAddress { unit: string; }extends关键字会复制被扩展类型的所有成员再添加新成员。它既减少了样板代码也明确表达了这两个类型在某种程度上相关的信息。interface还可以同时从多个类型扩展interface Colorful { color: string; } interface Circle { radius: number; } interface ColorfulCircle extends Colorful, Circle {} const cc: ColorfulCircle { color: red, radius: 42, };交叉类型 运算符除了interface的extends组合现有对象类型的另一种方式是交叉类型intersection types使用运算符type ColorfulCircle Colorful Circle;交叉类型的结果拥有参与交叉的所有类型的成员。把它用在函数参数上时调用方必须一次提供完整形状function draw(circle: Colorful Circle) { console.log(颜色是${circle.color}); console.log(半径是${circle.radius}); } draw({ color: 蓝, radius: 42 }); // 正常 // errors: 2345 draw({ color: 红, raidus: 42 }); // 错误拼写错误的 raidus接口 vs. 交叉类型两者都用来组合类型主要区别在于冲突处理使用interface extends时同名属性会被合并覆盖而交叉类型会把冲突的属性合并为更复杂的类型例如把两个string与number合并成never之类的受限类型。这种差异通常是你选择interface还是交叉类型的主要依据——当你明确需要合并语义时选接口需要叠加语义时选交叉类型。日常实践中interface适合描述对象形状并希望获得声明合并能力交叉类型则更适合组合结构未知或由多个来源拼凑的类型。泛型对象类型设想一个Box类型它想装任意内容interface Box { contents: any; }用any能工作但失去类型安全改用unknown则使用时必须做预防性检查或类型断言interface Box { contents: unknown; } let x: Box { contents: hello world }; if (typeof x.contents string) { console.log(x.contents.toLowerCase()); // 类型收窄后安全 } console.log((x.contents as string).toLowerCase()); // 或者用断言为每种内容分别创建类型也是一种办法但代价是大量样板代码——不仅要有NumberBox、StringBox、BooleanBox还要为它们各写一套函数重载// errors: 2322 interface NumberBox { contents: number; } interface StringBox { contents: string; } interface BooleanBox { contents: boolean; } 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; }更优雅的方案是声明带类型参数的泛型Boxinterface BoxType { contents: Type; }可以把它读作Type类型的Box是contents类型为Type的东西。引用Box时必须给出类型参数let box: Boxstring;当 TypeScript 看到Boxstring时会把BoxType中每个Type替换为string效果等价于{ contents: string }——也就是之前的StringBox。因为Type可以被替换为任何类型Box是可复用的需要新类型的盒子时根本不必再声明新类型interface Apple { /* ... */ } // 等同于 { contents: Apple } type AppleBox BoxApple;泛型还让我们可以用泛型函数彻底消除重载function setContentsType(box: BoxType, newContents: Type) { box.contents newContents; }类型别名同样可以是泛型的上面的Box接口可以改写为type BoxType { contents: Type }。而且由于类型别名不像interface那样只能描述对象类型我们还能写出更灵活的通用辅助类型type OrNullType Type | null; type OneOrManyType Type | Type[]; type OneOrManyOrNullType OrNullOneOrManyType; type OneOrManyOrNullStrings OneOrManyOrNullstring;Array 类型泛型对象类型通常是独立于元素类型的容器类型。整个手册中反复出现的Array就是典型例子number[]和string[]其实是Arraynumber、Arraystring的简写function doSomething(value: Arraystring) { // ... } let myArray: string[] [hello, world]; doSomething(myArray); doSomething(new Array(hello, world)); // 两种写法都可以Array本身就是泛型类型从它的声明可以看出典型的容器结构这里只摘录部分成员interface ArrayType { /** * 获取或设置数组的长度。 */ length: number; /** * 从数组中移除最后一个元素并返回它。 */ pop(): Type | undefined; /** * 向数组追加新元素并返回数组的新长度。 */ push(...items: Type[]): number; // ... }现代 JavaScript 还有MapK, V、SetT、PromiseT等泛型数据结构——正因为它们是泛型的才能适用于任何类型的集合。ReadonlyArray 类型ReadonlyArray描述不应该被修改的数组// errors: 2339 function doStuff(values: ReadonlyArraystring) { const copy values.slice(); // 可以读取 console.log(The first value is ${values[0]}); values.push(hello!); // 错误ReadonlyArray 上没有 push }与属性的readonly修饰符类似它主要是一个表达意图的工具看到返回ReadonlyArray的函数说明它承诺不改内容看到接受ReadonlyArray的函数说明可以放心把任意数组传给它。与Array不同ReadonlyArray没有构造函数// errors: 2693 new ReadonlyArray(red, green, blue); // 错误但可以把普通Array赋给它const roArray: ReadonlyArraystring [red, green, blue];TypeScript 为ReadonlyArrayType提供了简写readonly Type[]// errors: 2339 function doStuff(values: readonly string[]) { const copy values.slice(); values.push(hello!); // 错误 }与属性的readonly不同普通Array与ReadonlyArray之间的可赋值性不是双向的readonly数组可以赋给普通数组但反之不行——普通数组不能赋给readonly数组因为那会放弃对可变性的保证// errors: 4104 let x: readonly string[] []; let y: string[] []; x y; // 可行 y x; // 错误readonly string[] 不可赋给 string[]元组类型元组类型是另一种Array类型它精确知道包含多少个元素、以及每个特定位置的元素类型type StringNumberPair [string, number];StringNumberPair描述一个数组索引0是string索引1是number。它在运行时没有额外表示但对类型系统至关重要function doSomething(pair: [string, number]) { const a pair[0]; // 类型为 string const b pair[1]; // 类型为 number // ... } doSomething([hello, 42]);超出已知元素数量的索引会报错// errors: 2493 function doSomething(pair: [string, number]) { const c pair[2]; // 错误元素长度越界 }元组还可以配合 JavaScript 的数组解构function doSomething(stringHash: [string, number]) { const [inputString, hash] stringHash; console.log(inputString); // 类型为 string console.log(hash); // 类型为 number }元组类型在每个元素含义显而易见的高度基于约定的 API 中非常有用解构时可以随意命名变量。但显而易见因人而异——如果元素含义不够直观再三考虑使用带描述性属性名的对象可能更好。实际上简单的元组类型与声明了特定索引属性 数字字面量length的Array版本是等价的interface StringNumberPair { // 特别的属性 length: 2; 0: string; 1: number; // 其他 Arraystring | number 的成员... slice(start?: number, end?: number): Arraystring | number; }元组元素还可以通过问号?标记为可选可选元素只能出现在末尾并且会影响length的类型type Either2dOr3d [number, number, number?]; function setCoordinate(coord: Either2dOr3d) { const [x, y, z] coord; // z 的类型为 number | undefined console.log(所给坐标有 ${coord.length} 个维度); // length 的类型为 2 | 3 }元组还可以包含剩余元素必须是数组/元组类型并且位置可以灵活变化type StringNumberBooleans [string, number, ...boolean[]]; type StringBooleansNumber [string, ...boolean[], number]; type BooleansStringNumber [...boolean[], string, number];StringNumberBooleans前两个元素是string、number后面可有任意数量的booleanStringBooleansNumber第一个是string然后是任意数量boolean最后是numberBooleansStringNumber开头是任意数量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变体语法与数组简写类似在前面加readonlyfunction doSomething(pair: readonly [string, number]) { // ... }readonly元组的任何属性都不允许写入// errors: 2540 function doSomething(pair: readonly [string, number]) { pair[0] hello!; // 错误 }大多数代码中元组创建后都不会被修改所以尽可能把元组注释为readonly是一个很好的默认选择。这一点在const断言场景尤其重要带const断言的数组字面量会被推断为readonly元组类型与可变元组参数不兼容// errors: 2345 let point [3, 4] as const; // 推断为 readonly [3, 4] function distanceFromOrigin([x, y]: [number, number]) { return Math.sqrt(x ** 2 y ** 2); } distanceFromOrigin(point); // 错误readonly 元组不能传给可变元组参数这里distanceFromOrigin本身从不修改元素但它声明的是可变元组[number, number]无法保证point的元素不被修改因此被拒绝——把参数改为readonly [number, number]即可解决。小结对象类型是 TypeScript 类型系统的基石三种声明方式满足从内联到复用的不同需求?、readonly、索引签名三个属性修饰符分别控制可选性、可写性与未知键多余属性检查在字面量赋值时拦住拼写错误等潜在 bugextends与交叉类型提供了两种组合思路泛型对象类型BoxType让容器代码一次编写、处处复用而Array、ReadonlyArray与元组则是这套机制最常用的内置实践。若想继续深入可依次阅读本手册后续章节泛型、映射类型 与索引访问类型它们都建立在本文的对象类型基础之上。赞分享文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载相关推荐TypeScript readonly 属性详解readonly 修饰符、Readonly 工具类型与只读索引签名TypeScript readonly 属性详解readonly 修饰符、Readonly 工具类型与只读索引签名 本篇技术文章基于 The Concise文档教程TypeScript 只读属性Readonly Properties完全指南用 readonly 修饰符构建不可变类型TypeScript 只读属性Readonly Properties完全指南用 readonly 修饰符构建不可变类型 本文基于《The Concise文档教程PHPStan 错误标识符 empty.property对已知类型属性使用 empty() 的冗余检查详解PHPStan 错误标识符 empty.property对已知类型属性使用 empty 的冗余检查详解 导读 empty.property 是 PHPStan开发工具代码质量静态分析上一篇Floodlight控制器架构深度剖析核心模块与组件工作原理下一篇DoWhy 因果推断实战基于图模型的干预模拟与反事实计算What-If 查询指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表