改进TypeScript代码的5个技巧

257 阅读9分钟

hero.png

TypeScript作为一种类型更安全、维护性更强的JavaScript代码方式迅速流行。然而,它容易开始,却难以掌握高级特性;本文将介绍五个技巧,这些技巧将帮助您改进TypeScript代码。

阅读本文你将掌握以下5种技巧:

  1. 使用类型作为集合
  2. 使用联合判别作为可选字段替代
  3. 使用类型谓语精确类型
  4. 使用枚举优化代码
  5. 使用泛型提高代码灵活性

注:阅读本文中提供的代码示例,可以使用官方的 TypeScript 练习场


1.使用类型作为集合

集合理论:彼此不同且具有一些共同属性的元素的集合。

集合理论可以帮助您更好地推断TypeScript类型行为。类型是变量可以保存的一组可能值。例如,number类型是数字的集合。

let x: number; // 一组所有可能的数字
x = 123 // 123是个可能的数字
x = 'Hello' // 会抛出错误,因为hello是字符串,而不是数字

从上面的代码中,变量x是类型number的成员,因为它可以保存一个数字,在本例中是123。但是,尝试分配string,例如"Hello",会触发报错,因为x不是string类型的成员。因此,我们可以说x是类型number的子集。

此外,如果两个类型不相同,要分配成功,就会发生类型转换过程。类型转换是覆盖类型的过程。这个过程以两种形式发生:向上转换和向下转换。

在向上转换过程中,更具体类型的变量被分配给更通用类型的变量。为了更好地解释这一点,请考虑下面代码中具有不同类型的两个变量:

let name: string = 'John';
let nameLiteral: 'Patrick' | 'James' | 'Wood' = 'Patrick';

name = nameLiteral; // 向上转换被允许

变量name具有string类型,变量nameLiteral具有三个特定字符串类型:PatrickJamesWood。允许将nameLiteral赋值给name,因为nameLiteral的类型是name类型的适当子集,这使得它是类型安全的。换句话说,PatrickJamesWood都是字符串,由于namestring类型,TypeScript编译器不会发现问题。

但是,反转赋值将向下转换,这通常是不允许的。这是因为name是比nameLiteral Patrick James Wood 更大的string集合

let name: string = 'John';
let nameLiteral: 'Patrick' | 'James' | 'Wood' = 'Patrick';

nameLiteral = name; // 向下转换不被允许

集合运算符的概念,如unionintersectiondifferencecomplements,也可以用来将现有集合组合成新集合,以理解TypeScript中的类型组合。

然而,TypeScript只使用其中两个运算符作为类型运算符:|运算符用于联合,&运算符用于交集。然而,当使用它们组合类型(接口)时,它们的行为与直觉相反。

考虑下面的代码片段:

type User = {
   getId(): number;   
   getName(): string;
}

type Admin = {
   getId(): number;
   getUserName(): string;
}

declare function Person(): User | Admin;

const person = Person()

person.getId() // succeeds
person.getName() //fails
person.getUsername() // fails

TypeScript中的|运算符称为联合类型运算符,对于习惯于将|运算符视为其他上下文(例如布尔逻辑)中的逻辑OR运算符的开发人员来说,有时会产生误导。相反,它创建了一个包含组成类型共享的属性和方法交集的类型。

因此,UserAdmin的联合将只返回两者共有的属性和方法。

另一方面,将&运算符视为逻辑AND (&&)也可能会产生误导。

type User = {
   getId(): number;   
   getName(): string;
}

type Admin = {
   getId(): number;
   getUserName(): string;
}

declare function Person(): User & Admin;

const person = Person()

person.getId() // succeeds
person.getName() //succeeds
person.getUsername() //succeeds

总之,TypeScript中的|运算符表示一个联合类型,并采用所涉及类型的交集。相比之下,&运算符表示一个交集类型,并结合了多个类型的特征。理解这种区别对于避免误解TypeScript代码中的预期行为非常重要。


2.使用联合判别作为可选字段替代

假设您正在开发一个与各种API交互的TypeScript应用程序,并且您需要处理不同类型的响应,例如成功的数据检索或错误通知。为此,您通常可以使用可选字段或条件类型检查,如下例所示:

interface APIResponse {
 success: true | false;
 data?: number;
 error?: string;
}

function handleAPIResponse(response: APIResponse) {
 return (response.success) ? response.data : response.error
}

尽管可选字段可能看起来很方便,但重要的是要注意上述函数可能会返回undefined。这是因为dataerror值都可以是undefined的,也可以是接口属性中定义的特定值。要可视化这个概念,请参阅随附的图像以获得更清晰的理解。

-

相反,使用联合判别,我们可以在两个接口之间创建关系,从而提供更优雅和类型安全的解决方案:

interface SuccessResponse {
 success: true;
 data: number;
}

interface ErrorResponse {
 success: false;
 error: string;
}

interface APIResponse = SuccessResponse | ErrorResponse;

function handleAPIResponse(response: APIResponse) {
 (response.success) ? response.data: response.error
}

通过与联合判别建立关系,该函数将返回接口属性中定义的特定值,如下图所示:

-

我们不使用可选字段,而是通过使用联合判别来确保类型安全、管理特定于变体的逻辑以及提高代码易读性和可运维性。


3.使用类型谓语精确类型

想要在TypeScript中精确函数返回值的类型,谓语会派上用场。使用类型谓语,可以定义一个返回值为布尔类型的函数,以此来检查值是否属于特定类型。

谓语通常以variable is type的形式定义,用于函数的返回类型,如下所示:

function func(var: any): var is Type {
 return /* boolean expression */
}

在上面的示例函数中,func接受any类型的参数var并返回布尔值,var is Type表达式表示其返回值是类型谓语。

现在您了解了它的语法,让我们探索如何在实践中使用类型谓语:

type Fish = {
 swim:() => void
}

type Bird = {
 fly:() => void
}

function isFish( pet: Fish | Bird ): pet is Fish {
 return (pet as Fish).swim !== undefined 
}

在上面的例子中,我们定义了两种类型Fish有一个方法swimBird有一个方法fly

通过在函数isFish中指示类型谓语pet is Fish,我们向编译器表示,如果表达式(pet as Fish).swim !== undefined为真,也就是说,如果petswim方法,那么该值将属于Fish类型;否则它将被归类为Bird类型。这个概念非常方便,因为它允许编译器缩小类型范围,保证我们只处理控制语句中的特定类型。

通过使用此构造,我们可以将准确的类型传递给编译器,使其能够理解我们正在使用的类型,如下所示:

function getFood(pet: Fish | Bird) {
   if(isFish(pet)) {
       pet // if true, pet is of type Fish
       return 'fish food' 
   }
   else {
       pet //if false, pet is of type Bird
       return 'bird food'
   }
}

通过使用类型谓语,可以向编译器提供有关变量类型的附加信息;这有助于编译器进行精确的类型推断。这也可以帮助我们作为开发人员更快的调试。


4.使用枚举优化代码

在TypeScript中,枚举是能够编写一组命名常量的数据类型。枚举将相关值定义为numbersstrings。由于它们允许对相关值进行分组,因此这是组织代码的一种非常有用的方式,有助于类型安全。

要定义enum,请在enum关键字后跟其名称,然后在花括号中的命名常量,如下所示:

enum Color {
  Red,
  Green,
  Blue
}
// 使用
let color = Color.Red; 

默认情况下,第一个enum键默认值为0,除非另有定义(它也可以定义为字符串或分配自定义数值),然后为每个成员递增1。这就是上面的示例在幕后的样子:

enum Color {
  Red = 0,
  Green = 1,
  Blue = 2,
}

您还可以决定枚举的第一个成员的数值,然后其他成员从该值增加1

enum Color {
   Red = 1,
   Green,
   Blue
}

上图翻译为Red = 1Green = 2Blue = 3

Enums在有限数量的常量值情况下特别有用,例如一周中的几天。使用enum可以提供一种更结构化,类型更安全的方法来处理这些值:

enum Weekdays {
 Sunday = 1,
 Monday
 Tuesday,
 Wednesday,
 Thursday,
 Friday,
 Saturday
}

了解有关枚举的更多信息,请查看这 篇文章。


5.使用泛型提高代码灵活性

TypeScript的泛型允许开发可重用的、类型安全的组件。这些组件可以处理各种数据类型。通过特定的开发函数、类和接口,就可以对任何数据类型进行操作,增加了编程的灵活性。

泛型在TypeScript代码中声明为<T>,其中T代表已传入的类型 - 您可以将<T>解释为类型T的泛型作为将在生成结构实例时定义的类型的占位符,T在这种情况下的作用类似于参数在函数中的作用。

示例如下,我们将声明一个泛型类型为T的函数-类型T也是参数和返回值的类型,如下所示:

function func<T>(value: T):T {
  return value;
}

const result = func('Hello John');

T现在具有类型Hello John,传递给函数调用的确切参数:

-

这也适用于变量result

-

但是,您可能希望您的代码返回所需的类型。那么可以通过将所需类型设置为泛型类型参数来指定,如下所示:

function func<T>(value: T): T {
 return value;
}
const result = func<string>('Hello John');

在上面的示例中,类型T现在是类型string

-

泛型允许您构建可重用的、适应性强的组件,这些组件可以处理各种数据类型,而不会影响类型的安全性或代码易读性。因此,您可以根据需要快速调整和扩展组件以使用新的数据类型,从而随着时间的推移赋予代码更大的灵活性和可维护性。


结论

总之,TypeScript是编写可扩展与可维护代码的强大工具。通过合并类型注释、联合类型、类型谓语、枚举和泛型,大大提高了代码类型的安全性和灵活性。类型注释有助于及早发现错误并提供更好的文档,而联合类型和枚举使代码更具可读性,代码一目了然。类型谓语和泛型为代码增加了更多的灵活性,允许您创建可重用、可适应的函数和

然而,以上这几点只是TypeScript特性和优势的冰山一角。希望可以帮助您更多地了解该语言并找到改进代码的新方法。TypeScript提供的强大的工具、紧凑的类型以及对JavaScript代码特性的支持,您可以更好更快的生成代码。

本文为译文 原文链接