TypeScript魔术学校:泛型和类型扩展

我们今天翻译的文章的作者说TypeScript非常棒。 刚开始使用TS时,他真的很喜欢这种语言固有的自由。 程序员在使用TS特定​​的机制上投入的精力越多,他将获得的利益就越大。 然后,他仅定期使用类型注释。 有时他会利用代码完成和编译器提示的机会,但主要是依靠自己对解决任务的看法。

随着时间的流逝,该材料的作者意识到,每次绕过在编译阶段检测到的错误时,他都会在代码中放置一个定时炸弹,该炸弹可能在程序执行期间爆炸。 每次他使用as any一种简单的结构来“努力”解决错误时,他都不得不花很多时间进行艰苦的调试。



结果,他得出的结论是最好不要这样做。 他与编译器交了朋友,开始注意他的提示。 编译器会在代码中发现问题,并在可能造成真正危害之前就将其报告。 该文章的作者将自己视为开发人员,他意识到编译器是他最好的朋友,因为它可以保护他免受自己的侵害。 怎能不回想起阿不思·邓布利多的话:“与敌人大声说话需要很大的勇气,但与朋友大声说话至少要有勇气。”

不管编译器有多好,都不总是很容易取悦。 有时,避免使用any类型非常困难。 有时似乎any都是解决某些问题的唯一合理方法。

本材料重点介绍两种情况。 通过避免在其中使用any类型,可以确保代码的类型安全性,打开其重用的可能性并使其直观。

泛型


假设我们正在处理学校的数据库。 我们编写了一个非常方便的辅助函数getBy 。 为了获得代表学生姓名的对象,我们可以使用getBy(model, "name", "Harry")形式的命令getBy(model, "name", "Harry") 。 让我们看一下这种机制的实现(这里,为了不使代码复杂化,数据库由一个普通数组表示)。

 type Student = { name: string; age: number; hasScar: boolean; }; const students: Student[] = [ { name: "Harry", age: 17, hasScar: true }, { name: "Ron", age: 17, hasScar: false }, { name: "Hermione", age: 16, hasScar: false } ]; function getBy(model, prop, value) {   return model.filter(item => item[prop] === value)[0] } 

如您所见,我们有一个很好的函数,但是它不使用类型注释,而且它们的缺失也意味着此类函数不能称为类型安全的。 修复它。

 function getBy(model: Student[], prop: string, value): Student | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "name", "Hermione") // result: Student 

所以我们的功能看起来已经好多了。 现在,编译器知道预期的结果类型,这将在以后派上用场。 但是,为了实现使用类型的安全工作,我们牺牲了重用该功能的可能性。 如果我们需要使用它来获取其他实体怎么办? 不可能不能以任何方式改进此功能。 确实是。

与其他强类型语言一样,在TypeScript中,我们可以使用泛型,也称为“泛型”,“通用类型”,“泛化”。

泛型类似于常规变量,但是它包含一些类型定义,而不是某个值。 我们重写函数的代码,以便使用通用类型T代替Student类型T

 function getBy<T>(model: T[], prop: string, value): T | null {   return model.filter(item => item[prop] === value)[0] } const result = getBy<Student>(students, "name", "Hermione") // result: Student 

美女! 现在,该功能非常适合重用,而类型安全性仍在我们这边。 请注意,在上面的代码片段的最后一行中,使用通用TStudent类型是如何明确设置的。 这样做是为了使示例尽可能清晰,但实际上,编译器可以独立派生所需的类型,因此在以下示例中,我们将不进行此类类型优化。

因此,现在我们有了适合重用的可靠辅助函数。 但是,它仍然可以改进。 如果输入第二个参数时出错,而不是"name"出现"naem"怎么办? 该函数的行为就像您要找的学生根本不在数据库中一样,并且最不愉快的是,它不会产生任何错误。 这可能会导致长期调试。

为了防止此类错误,我们引入了另一个通用类型P 此外,必须将P作为类型T的键,因此,如果在此处使用Student ,则必须将P作为字符串"name""age""hasScar" 。 这是操作方法。

 function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "naem", "Hermione") // Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'. 

使用泛型和keyof是一个非常强大的技巧。 如果在支持TypeScript的IDE中编写程序,则通过输入参数可以利用自动完成功能,这非常方便。

但是,我们尚未完成getBy函数的工作。 她有第三个论点,我们尚未确定。 这根本不适合我们。 直到现在,我们仍无法预先知道应该是哪种类型,因为它取决于我们作为第二个参数传递的内容。 但是现在,由于我们拥有类型P ,所以我们可以动态地推断出第三个参数的类型。 第三个参数的类型最终将为T[P] 。 结果,如果TStudent ,并且P"age" ,则T[P]将是number类型。

 function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "age", "17") // Error: Argument of type '"17"' is not assignable to parameter of type 'number'. const anotherResult = getBy(students, "hasScar", "true") // Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'. const yetAnotherResult = getBy(students, "name", "Harry") //      

我希望现在您对如何在TypeScript中使用泛型有一个绝对清晰的了解,但是如果您想对要使用此处讨论的代码进行实验的所有内容进行很好的实验,则可以在此处查看

扩展现有类型


有时,我们可能会遇到需要向无法更改其代码的接口添加数据或功能的需求。 您可能需要更改标准对象,例如-向window对象添加一些属性,或扩展某些外部库(如Express的行为。 在这两种情况下,您都无法直接影响要使用的对象。

我们将通过将您已经知道的getBy函数添加到Array原型中来解决该问题。 这将使我们能够使用此功能来构建更准确的句法构造。 目前,我们并不是在讨论扩展标准对象是好是坏,因为我们的主要目标是研究所考虑的方法。

如果我们尝试向Array原型添加函数,编译器将不会非常喜欢这样:

 Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; // Error: Property 'getBy' does not exist on type 'any[]'. const bestie = students.getBy("name", "Ron"); // Error: Property 'getBy' does not exist on type 'Student[]'. const potionsTeacher = (teachers as any).getBy("subject", "Potions") //  ...   ? 

如果我们尝试通过定期使用as any结构来保证编译器的安全,则将使我们所获得的一切无效。 编译器将保持沉默,但是您可以忘记使用类型的安全工作。

最好扩展Array类型,但是在执行此操作之前,让我们先讨论一下TypeScript如何处理代码中存在相同类型的两个接口时的情况。 这里采用了一种简单的行动方案。 如果可能,将合并广告。 如果无法将它们组合在一起,系统将给出错误信息。

因此,此代码有效:

 interface Wand { length: number } interface Wand {   core: string } const myWand: Wand = { length: 11, core: "phoenix feather" } //  ! 

而这个不是:

 interface Wand { length: number } interface Wand {   length: string } // Error: Subsequent property declarations must have the same type.  Property 'length' must be of type 'number', but here has type 'string'. 

现在,在处理了这个问题之后,我们看到我们面临着一个相当简单的任务。 即,我们需要做的就是声明Array<T>接口并向其添加getBy函数。

 interface Array<T> {  getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; const bestie = students.getBy("name", "Ron"); //   ! const potionsTeacher = (teachers as any).getBy("subject", "Potions") //     

请注意,您可能会在模块文件中编写大多数代码,因此,要更改Array接口,您将需要访问全局范围。 您可以通过将类型定义放置在declare global 。 例如,像这样:

 declare global {   interface Array<T> {       getBy<P extends keyof T>(prop: P, value: T[P]): T | null;   } } 

如果要扩展外部库的接口,则很可能需要访问该库的namespace 。 这是一个如何将userId字段添加到Express库中的Request的示例:

 declare global { namespace Express {   interface Request {     userId: string;   } } } 

您可以在此处试用本节中的代码。

总结


在本文中,我们研究了在TypeScript中使用泛型和类型扩展的技术。 我们希望您今天所学到的内容能够帮助您编写可靠,可理解且类型安全的代码。

亲爱的读者们! 您对TypeScript中的任何类型感觉如何?

Source: https://habr.com/ru/post/zh-CN426729/


All Articles