ARTICLE DETAIL

资讯详情

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

TypeScript 接口完全指南:从结构类型契约到高级应用模式

TypeScript 接口完全指南:从结构类型契约到高级应用模式 文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载本文是 TypeScript 使用手册中文版「手册」章节的接口专题对应文档 zh/handbook/interfaces.md系统讲解 TypeScript 接口的核心原则——结构性子类型化结构化类型/鸭式辨型覆盖接口定义、可选属性、只读属性、额外属性检查、函数类型、可索引类型、类类型、接口继承、混合类型与接口继承类等全部要点。读完本文你将掌握用接口为代码与第三方代码定义契约的完整方法论并能理解 TypeScript 类型检查器在接口兼容性上的底层判定逻辑直接支撑你在实际项目中编写健壮、可读、可复用的类型定义。介绍接口与结构性子类型化TypeScript 的核心原则之一是对值所具有的结构进行类型检查。它有时被称做鸭式辨型法或结构性子类型化。在 TypeScript 里接口的作用就是为这些类型命名并为你的代码或第三方代码定义契约。这种设计与其他语言如 C#、Java的名义类型系统有着根本差异在基于名义类型的语言中类型兼容性依赖明确的声明与类型名称而 TypeScript 的类型系统只关注值的外形shape。这一点在 类型兼容性 一章中有更详细的展开——例如一个没有显式声明实现Named接口的Person类只要其结构成员与接口匹配就可以赋给Named类型的变量这在名义类型语言中会直接报错。TypeScript 之所以采用结构子类型正是为了贴合 JavaScript 广泛使用匿名对象函数表达式、对象字面量的典型写法。接口初探下面通过一个简单示例来观察接口是如何工作的function printLabel(labeledObj: { label: string }) { console.log(labeledObj.label); } let myObj { size: 10, label: Size 10 Object }; printLabel(myObj);类型检查器会查看printLabel的调用它有一个参数并要求这个对象参数有一个名为label、类型为string的属性。需要注意我们传入的对象参数实际上会包含很多属性这里多了size但编译器只会检查那些必需的属性是否存在、类型是否匹配。也就是说只要传入的对象满足必要条件它就是被允许的。不过有些时候 TypeScript 并不会这么宽松比如对象字面量的额外属性检查下面会稍做讲解。下面重写上面的例子这次使用接口来描述必须包含一个label属性且类型为stringinterface LabeledValue { label: string; } function printLabel(labeledObj: LabeledValue) { console.log(labeledObj.label); } let myObj { size: 10, label: Size 10 Object }; printLabel(myObj);LabeledValue接口就好比一个名字用来描述上面例子里的要求。它代表了有一个label属性且类型为string的对象。需要注意的是我们在这里并不能像在其它语言里一样说传给printLabel的对象实现了这个接口——我们只会去关注值的外形。还有一点值得提的是类型检查器不会去检查属性的顺序只要相应的属性存在并且类型正确就可以。从接口语法上看interface声明只创建类型实体不创建运行时值这在 声明合并 的声明实体表中可以印证——接口属于仅创建类型的声明编译后不会产生任何 JavaScript 输出因此接口的使用成本为零。可选属性接口里的属性不全都是必需的。有些是只在某些条件下存在或者根本不存在。可选属性在应用option bags模式时很常用即给函数传入的参数对象中只有部分属性被赋值。下面是应用了 option bags 的例子interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare { color: white, area: 100 }; if (config.color) { newSquare.color config.color; } if (config.width) { newSquare.area config.width * config.width; } return newSquare; } let mySquare createSquare({ color: black });带有可选属性的接口与普通的接口定义差不多只是在可选属性名字定义的后面加一个?符号color?: string表示该属性可以缺失。可选属性的好处有二对可能存在的属性进行预定义调用方可以只传入部分配置接口依然完整描述了可能包含哪些键捕获引用了不存在的属性时的错误比如我们故意将createSquare里的color属性名拼错就会得到一个错误提示interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare { color: white, area: 100 }; if (config.clor) { // Error: Property clor does not exist on type SquareConfig newSquare.color config.clor; } if (config.width) { newSquare.area config.width * config.width; } return newSquare; } let mySquare createSquare({ color: black });编译器会精确地指出config.clor不存在于SquareConfig类型上这种打错一个字母立刻被揪出来的能力正是接口对配置对象类代码的价值所在。只读属性一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用readonly来指定只读属性interface Point { readonly x: number; readonly y: number; }你可以通过赋值一个对象字面量来构造一个Point。赋值后x和y再也不能被改变了let p1: Point { x: 10, y: 20 }; p1.x 5; // error!TypeScript 还具有ReadonlyArrayT类型它与ArrayT相似只是把所有可变方法如push、pop、splice等去掉了因此可以确保数组创建后再也不能被修改let a: number[] [1, 2, 3, 4]; let ro: ReadonlyArraynumber a; ro[0] 12; // error! ro.push(5); // error! ro.length 100; // error! a ro; // error!上面代码的最后一行很关键就算把整个ReadonlyArray赋值到一个普通数组也是不可以的——只读性在赋值方向上也得到了保护。但你可以用类型断言重写这个限制a ro as number[];readonlyvsconst最简单判断该用readonly还是const的方法是看把它做为变量使用还是做为属性使用做为变量使用的话用const若做为属性则使用readonly。两者的作用域不同const约束变量引用的重新绑定readonly约束属性值的再次赋值。额外的属性检查我们在第一个例子里使用了接口TypeScript 让我们传入{ size: number; label: string; }到仅期望得到{ label: string; }的函数里。我们已经学过了可选属性并且知道它们在 option bags 模式里很有用。然而天真地将这两者结合的话就会像在 JavaScript 里那样搬起石头砸自己的脚。比如拿createSquare例子来说interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... } let mySquare createSquare({ colour: red, width: 100 });注意传入createSquare的参数拼写为colour而不是color。在 JavaScript 里这会默默地失败——config.colour永远是undefined拼写错误被静默吞掉。你可能会争辩这个程序已经正确地类型化了width属性是兼容的不存在color属性而且额外的colour属性是无意义的。然而TypeScript 会认为这段代码可能存在 bug。对象字面量会被特殊对待而且会经过额外属性检查当将它们赋值给变量或作为参数传递的时候如果一个对象字面量存在任何目标类型不包含的属性你就会得到一个错误// error: Object literal may only specify known properties, but colour does not exist in type SquareConfig. Did you mean to write color? let mySquare createSquare({ colour: red, width: 100 });注意编译器甚至会给出 Did you mean to write color? 这样的纠错提示。这正是接口作为契约的严苛一面契约内允许可选契约外不允许凭空多出键。绕开这些检查非常简单。最简便的方法是使用类型断言let mySquare createSquare({ width: 100, opacity: 0.5 } as SquareConfig);然而最佳的方式是添加一个字符串索引签名前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。如果SquareConfig带有上面定义的类型的color和width属性并且还会带有任意数量的其它属性那么我们可以这样定义它interface SquareConfig { color?: string; width?: number; [propName: string]: any; }我们稍后会讲到索引签名但在这我们要表示的是SquareConfig可以有任意数量的属性并且只要它们不是color和width那么就无所谓它们的类型是什么。还有最后一种跳过这些检查的方式这可能会让你感到惊讶它就是将这个对象赋值给另一个变量因为squareOptions不会经过额外属性检查额外属性检查只针对对象字面量所以编译器不会报错let squareOptions { colour: red, width: 100 }; let mySquare createSquare(squareOptions);但上面的方法只在squareOptions和SquareConfig之间有共同的属性时才好用——在这个例子中这个属性为width。如果变量间不存在共同的对象属性将会报错例如let squareOptions { colour: red }; let mySquare createSquare(squareOptions);这里squareOptions与目标类型没有任何公共成员结构上完全无法兼容因此即使绕过对象字面量检查也会被常规结构类型检查拦截。要留意在像上面一样的简单代码里你可能不应该去绕开这些检查。对于包含方法和内部状态的复杂对象字面量来讲你可能需要使用这些技巧但大部分额外属性检查错误是真正的 bug。就是说如果你遇到了额外类型检查出的错误比如 option bags 场景你应该去审查一下你的类型声明。在这里如果确实支持传入color或colour属性到createSquare你应该修改SquareConfig定义来体现出这一点而不是绕过检查。函数类型接口能够描述 JavaScript 中对象拥有的各种各样的外形。除了描述带有属性的普通对象外接口也可以描述函数类型。为了使用接口表示函数类型我们需要给接口定义一个调用签名call signature。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型interface SearchFunc { (source: string, subString: string): boolean; }这样定义后我们可以像使用其它接口一样使用这个函数类型的接口。下例展示了如何创建一个函数类型的变量并将一个同类型的函数赋值给这个变量let mySearch: SearchFunc; mySearch function (source: string, subString: string) { let result source.search(subString); return result -1; };对于函数类型的类型检查来说函数的参数名不需要与接口里定义的名字相匹配。比如我们使用下面的代码重写上面的例子let mySearch: SearchFunc; mySearch function (src: string, sub: string): boolean { let result src.search(sub); return result -1; };函数的参数会逐个进行检查要求对应位置上的参数类型是兼容的。如果你不想指定类型TypeScript 的类型系统会推断出参数类型因为函数直接赋值给了SearchFunc类型变量函数的返回值类型也是通过其返回值推断出来的此例是false和truelet mySearch: SearchFunc; mySearch function (src, sub) { let result src.search(sub); return result -1; };如果让这个函数返回数字或字符串类型检查器会警告我们函数的返回值类型与SearchFunc接口中的定义不匹配let mySearch: SearchFunc; // error: Type (src: string, sub: string) string is not assignable to type SearchFunc. // Type string is not assignable to type boolean. mySearch function (src, sub) { let result src.search(sub); return string; };函数赋值时的参数兼容性参数双向协变/逆变、返回值协变等细节在 类型兼容性 的比较两个函数一节有系统性说明。函数类型的接口非常适合描述回调、处理器等可调用对象是设计库 API 时的常用手段。可索引的类型与使用接口描述函数类型差不多我们也可以描述那些能够通过索引得到的类型比如a[10]或ageMap[daniel]。可索引类型具有一个索引签名它描述了对象索引的类型还有相应的索引返回值类型。看一个例子interface StringArray { [index: number]: string; } let myArray: StringArray; myArray [Bob, Fred]; let myStr: string myArray[0];上面例子里我们定义了StringArray接口它具有索引签名。这个索引签名表示了当用number去索引StringArray时会得到string类型的返回值。TypeScript 支持两种索引签名字符串和数字。可以同时使用两种类型的索引但是数字索引的返回值必须是字符串索引返回值类型的子类型。这是因为当使用number来索引时JavaScript 会将它转换成string然后再去索引对象——也就是说用100一个number去索引等同于使用100一个string去索引因此两者需要保持一致。下面的错误示例展示了违反该规则的情形class Animal { name: string; } class Dog extends Animal { breed: string; } // 错误使用数值型的字符串索引有时会得到完全不同的 Animal! interface NotOkay { [x: number]: Animal; [x: string]: Dog; }如果数字索引返回Animal而字符串索引返回Dog那么myArray[100]被转换成100索引可能得到Animal而非Dog类型就不一致了。字符串索引签名能够很好地描述dictionary字典模式并且它们也会确保所有属性与其返回值类型相匹配。因为字符串索引声明了obj.property和obj[property]两种形式都可以。下面的例子里name的类型与字符串索引类型不匹配所以类型检查器给出一个错误提示interface NumberDictionary { [index: string]: number; length: number; // 可以length 是 number 类型 name: string; // 错误name 的类型与索引类型返回值的类型不匹配 }但如果索引签名是包含属性类型的联合类型那么使用不同类型的属性就是允许的interface NumberOrStringDictionary { [index: string]: number | string; length: number; // ok, length is a number name: string; // ok, name is a string }最后你可以将索引签名设置为只读这样就防止了给索引赋值interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray [Alice, Bob]; myArray[2] Mallory; // error!你不能设置myArray[2]因为索引签名是只读的。索引签名在现代 TypeScript 的进阶用法中也是基础——例如 keyof 类型运算符 中指出当类型具有string或number索引签名时keyof会返回相应的string | number而 索引访问类型 则展示了如何基于接口成员类型做进一步推导如Person[age]。类类型实现接口与 C# 或 Java 里接口的基本作用一样TypeScript 也能够用它来明确地强制一个类去符合某种契约interface ClockInterface { currentTime: Date; } class Clock implements ClockInterface { currentTime: Date new Date(); constructor(h: number, m: number) {} }你也可以在接口中描述一个方法在类里实现它如同下面的setTime方法一样interface ClockInterface { currentTime: Date; setTime(d: Date): void; } class Clock implements ClockInterface { currentTime: Date new Date(); setTime(d: Date) { this.currentTime d; } constructor(h: number, m: number) {} }接口描述了类的公共部分而不是公共和私有两部分。它不会帮你检查类是否具有某些私有成员。关于类的继承、访问修饰符public/private/protected、抽象成员等更完整的类体系知识可参见 类 一章。类静态部分与实例部分的区别当你操作类和接口的时候你要知道类是具有两个类型的静态部分的类型和实例的类型。你会注意到当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误interface ClockConstructor { new (hour: number, minute: number); } class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) {} }这里因为当一个类实现了一个接口时只对其实例部分进行类型检查。constructor存在于类的静态部分所以不在检查的范围内。因此我们应该直接操作类的静态部分。看下面的例子我们定义了两个接口——ClockConstructor为构造函数所用、ClockInterface为实例方法所用。为了方便我们定义一个构造函数createClock它用传入的类型创建实例interface ClockConstructor { new (hour: number, minute: number): ClockInterface; } interface ClockInterface { tick(): void; } function createClock( ctor: ClockConstructor, hour: number, minute: number ): ClockInterface { return new ctor(hour, minute); } class DigitalClock implements ClockInterface { constructor(h: number, m: number) {} tick() { console.log(beep beep); } } class AnalogClock implements ClockInterface { constructor(h: number, m: number) {} tick() { console.log(tick tock); } } let digital createClock(DigitalClock, 12, 17); let analog createClock(AnalogClock, 7, 32);因为createClock的第一个参数是ClockConstructor类型在createClock(AnalogClock, 7, 32)里会检查AnalogClock是否符合构造函数签名。这种构造器签名 实例接口分离的模式是 TypeScript 中实现工厂函数factory类型安全的标准做法。另一种简单方式是使用类表达式interface ClockConstructor { new (hour: number, minute: number); } interface ClockInterface { tick(); } const Clock: ClockConstructor class Clock implements ClockInterface { constructor(h: number, m: number) {} tick() { console.log(beep beep); } };继承接口和类一样接口也可以相互继承。这让我们能够从一个接口里复制成员到另一个接口里可以更灵活地将接口分割到可重用的模块里interface Shape { color: string; } interface Square extends Shape { sideLength: number; } let square {} as Square; square.color blue; square.sideLength 10;一个接口可以继承多个接口创建出多个接口的合成接口interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } let square {} as Square; square.color blue; square.sideLength 10; square.penWidth 5.0;接口的多继承语义上接近 高级类型 中介绍的交叉类型Shape PenStroke继承后的Square同时拥有所有父接口的成员形成合成契约。混合类型先前我们提过接口能够描述 JavaScript 里丰富的类型。因为 JavaScript 动态灵活的特点有时你会希望一个对象可以同时具有上面提到的多种类型。一个例子就是一个对象可以同时作为函数和对象使用并带有额外的属性interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter function (start: number) {} as Counter; counter.interval 123; counter.reset function () {}; return counter; } let c getCounter(); c(10); c.reset(); c.interval 5.0;Counter接口同时声明了调用签名(start: number): string、属性interval: number与方法reset(): void。在使用 JavaScript 第三方库的时候你可能需要像上面那样去完整地定义类型——例如许多 JS 库导出的可调用对象既可直接调用又挂载了属性都需要用混合类型接口来精确描述。接口继承类当接口继承了一个类类型时它会继承类的成员但不包括其实现。就好像接口声明了所有类中存在的成员但并没有提供具体实现一样。接口同样会继承到类的private和protected成员。这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时这个接口类型只能被这个类或其子类所实现implement。当你有一个庞大的继承结构时这很有用但要指出的是你的代码只在子类拥有特定属性时起作用。除了继承自基类子类之间不必相关联。例class Control { private state: any; } interface SelectableControl extends Control { select(): void; } class Button extends Control implements SelectableControl { select() {} } class TextBox extends Control { select() {} } class ImageControl implements SelectableControl { // Error: Class ImageControl incorrectly implements interface SelectableControl. // Types have separate declarations of a private property state. private state: any; select() {} }在上面的例子里SelectableControl包含了Control的所有成员包括私有成员state。因为state是私有成员所以只能够是Control的子类们才能实现SelectableControl接口——因为只有Control的子类才能够拥有一个声明于Control的私有成员state这对私有成员的兼容性是必需的。在Control类内部是允许通过SelectableControl的实例来访问私有成员state的。实际上SelectableControl就像Control一样并拥有一个select方法。Button和TextBox类可以视为SelectableControl的实现者因为它们都继承自Control并有select方法。而对于ImageControl类它有自身的私有成员state而不是通过继承Control得来的所以它不可以实现SelectableControl——编译器会以 Types have separate declarations of a private property state 明确指出二者各自的state声明互不相通。这一机制常被用来在大型继承体系中限定某类接口只能由特定基类家族实现。小结接口在 TypeScript 类型体系中的位置接口是 TypeScript 类型系统的地基性构件它贯穿于整个手册体系与 基础类型string、number、数组等相比接口让开发者能够为复合结构命名并复用与 类 配合接口可以描述类的公共契约implements并借助构造器签名与实例接口的分离实现工厂模式与 函数、泛型 组合接口可用于描述回调签名与泛型约束接口在 声明合并 中是最常见也最灵活的合并对象同名接口的成员会被自动合并且interface只产生类型实体、不产生运行时输出在 类型兼容性 的结构子类型规则下接口是判定两个值外形是否一致的基准在 高级类型 与 keyof 类型运算符、索引访问类型 中接口定义的成员与索引签名又是类型推导的重要输入。掌握本文介绍的接口定义、可选与只读属性、额外属性检查的绕过与规避、函数类型与可索引类型、类类型与接口继承你就拥有了用 TypeScript 为任意 JavaScript 外形建立精确契约的完整工具集能够在代码与第三方库之间写出既安全又灵活的类型边界。赞分享文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载相关推荐TypeScript高级类型应用Upterm接口设计与类型安全TypeScript高级类型应用Upterm接口设计与类型安全 在21世纪的终端模拟器开发中类型安全是保障复杂交互逻辑可靠性的关键。Upterm作为现代化终开发工具桌面应用TypeScript 匿名元组类型Anonymous Tuple Type完全指南用固定结构数组表达精确的类型契约TypeScript 匿名元组类型Anonymous Tuple Type完全指南用固定结构数组表达精确的类型契约 导读 元组Tuple是 TypeS文档教程Cycle.js与TypeScript高级类型构建类型安全的响应式应用Cycle.js与TypeScript高级类型构建类型安全的响应式应用 你是否在开发响应式应用时遇到过类型不匹配导致的运行时错误是否希望在编译阶段就能捕获数前端Web框架上一篇zls并发库并发编程库集成支持下一篇3大突破重新定义金融时间序列预测的智能引擎创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表