7个Typescript常见错误需要你注意避免发生

英文 | https://betterprogramming.pub/7-typescript-common-mistakes-to-avoid-581c30e514d6

翻译 | 杨小二

自 2012 年 10 月首次出现以来,TypeScript 获得了广泛的关注,它已成为 Web 开发领域真正的游戏规则改变者。尽管如此,有些人一直对使用它持怀疑态度。

将 TypeScript 添加到项目时,开发人员应该接受它而不是反对它,这一点很重要。

这可能会使你沮丧,以及在开发中TypeScript 会成为开发过程中的瓶颈。

但是,如果使用得当,拥有可读且易于维护的代码库就变得至关重要。

它具有强大的功能,例如映射类型、重载、类型推断、可选类型等,并且随着增量升级,这些功能每天都在变得更好。

为什么有些人会觉得 TypeScript 正在损害他们的生产力?我们怎样才能防止这种不好情况的发生?是否有一些我们可以采用的最佳实践做法?

在这里,我们将讨论使用 TypeScript 时最常见的错误。通过不迷恋这些常见的问题,我们将看到我们的生产力和代码可维护性的提高。

现在开始吧。

1、不启用严格模式

如果没有打开 TypeScript 严格模式,类型可能会过于松散,这将使我们的代码库类型安全性降低。它会给人错误的印象,因为有些人认为通过添加 TypeScript,所有TypeScript问题都会自动修复。

以后,我们将成为这些类型的受害者。我们最终可能会用补丁修复它们,而不是修复根本原因。这可能会导致你认为该工具做得不好。

我们如何启用严格模式?它通过在 tsconfig.json 文件中将 strict 参数设置为 true 来启用,如下所示:

{
  ...
  "compilerOptions": {
    "strict": true,
    ...
  },
  ...
}

启用strict模式将在钩子下启用:

noImplicitAny:此标志可防止我们使用推断的 any 公开合约。如果我们不指定类型并且无法推断,则默认为any。

noImplicitThis:它将防止 this 关键字的不必要的不安全用法。防止不需要的行为将使我们免于一些调试麻烦,如下所示:

class Book {
  pages: number;
  constructor(totalPages: number) {
    this.pages = totalPages;
  }


  isLastPageFunction() {
    return function (currentPage: number) {
      // ❌ 'this' here implicitly has type 'any' because it does not have a type annotation.
      return this.pages === currentPage;
    }
  }
}

alwaysStrict:这将确保在我们所有转换后的 JavaScript 文件中发出 use strict ,但编译器除外。这将提示 JavaScript 引擎代码应该在严格模式下执行。

strictBindCallApply:这将确保我们使用正确的参数调用 call 、 bind 和 apply 函数。让我们看一个例子:

const logNumber = (x: number) => {
  console.log(`number ${x} logged!`)
}


// ✅ works fine
logNumber.call(undefined, 10);


// ❌ error: Argument of type 'string' is not assignable to parameter of type 'number'.ts(2345)
logNumber.call(undefined, "10");

strictNullChecks:如果此标志关闭,则编译器会有效地忽略 undefined、null 和 false。松散的输入可能会导致运行时出现意外错误。让我们看一个例子:

interface Person {
  name: string | undefined;
  age: number;
}


const x: Person = { name: 'Max', age: 3 };


// ❌ Works with strictNullChecks off, which is lax
console.log(x.name.toLowerCase());




// ✅ Fails with strictNullChecks on as x.name could be undefined
console.log(x.name.toLowerCase());

strictFunctionTypes:启用此标志可确保更彻底地检查函数参数。

strictPropertyInitialization:当设置为 true 时,这将强制我们在构造函数中设置所有属性值。

正如所见,TypeScript 的严格变量是上述所有标志的简写。我们可以通过使用严格或逐步启用它们来启用它们。

更严格的类型将帮助我们在编译时捕获更多错误。

2、重新声明接口

在键入组件接口时,通常需要具有相同类型的一些不同接口变体。这些可以在一两个参数中变化。一个常见的错误是手动重新定义这些变体。这将导致:

  • 不必要的样板。

  • 需要多次更改。如果一个属性在一个地方发生变化,则需要将该更改传播到多个文件。

很久以前,TypeScript 发布了一个旨在解决此目的的功能:映射类型。它们让我们可以根据我们定义的一些规则,在现有类型的基础上创建新类型。这确实会导致更具可读性和声明性的代码库。

让我们看一个例子:

interface Book {
  author?: string;
  numPages: number;
  price: number;
}


// ✅ Article is a Book without a Page
type Article = Omit<Book, 'numPages'>;


// ✅ We might need a readonly verison of the Book Type
type ReadonlyBook = Readonly<Book>;


// ✅ A Book that must have an author
type NonAnonymousBook = Omit<Book, 'author'> & Required<Pick<Book, 'author'>>;

在上面的代码中,我们保留了一个单一的事实来源:Book 实体。它的所有变体都使用映射类型功能来表达,这大大减少了对代码进行类型化和维护的成本。

映射类型也可以应用于联合,如下所示:

type animals = 'bird' | 'cat' | 'crocodile';


type mamals = Exclude<animals, 'crocodile'>;
// 'bird' | 'cat'

TypeScript 附带以下映射类型:Omit、Partial、Readonly、Exclude、Extract、NonNullable、ReturnType。

我们可以创建自己的实用程序并在我们的代码库中重用它们。

3、不依赖类型推断

TypeScript 推理是这种编程语言最强大的工具之一。它为我们完成所有工作。我们只需要确保在尽可能少的干预下将所有部分加在一起。

实现这一目标的一个关键操作符是 typeof。它是一个类似于 JavaScript 的运算符。它不会返回 JavaScript 类型,而是返回 TypeScript 类型。使用这个操作数可以避免我们重新声明相同的类型。

让我们通过一个例子来看看:

const addNumber = (a: number, b: number) => a + b;


// ❌ you are hardcoding the type `number` instead of relying on what the function returns
const processResult = (result: number) => console.log(result);
processResult(addNumber(1, 1));




// ✅ result will be whatever the return type of addNumber function
// no need for us to redefine it
const processResult = (result: ReturnType<typeof addNumber>) => console.log(result);
processResult(addNumber(1, 1));

在上面的代码中,注意结果参数类型。最好依赖 ReturnType<typeof addNumber> 而不是添加数字类型。通过对数字类型进行硬编码,我们完成了编译器的工作。

最好使用适当的语法来表达我们的类型。TypeScript 将为我们完成繁重的工作。

让我们看一个虚拟示例:

// ❌ Sometimes for one of objects it is not necessary to define
// interfaces for it
interface Book {
  name: string,
  author: string
}


const book: Book = {
  name: 'For whom the bell tolls',
  author: 'Hemingway'
}


const printBook = (bookInstance: Book) => console.log(bookInstance)

请注意,Book 接口用于特定场景,甚至不需要创建接口。

通过依赖 TypeScript 的推理,代码变得不那么杂乱,更易于阅读。下面是一个例子:

// ✅ For simple scenarios we can rely on type inference
const book = {
  name: 'For whom the bell tolls',
  author: 'Hemingway'
}


const printBook = (bookInstance: typeof book) => console.log(bookInstance)

TypeScript 甚至有 infer 运算符,它可以与 Mapped Types 结合使用以从另一个类型中提取一个类型。

const array: number[] = [1,2,3,4];


// ✅ type X will be number
type X = typeof array extends (infer U)[] ? U : never;

在上面的例子中,我们可以看到如何提取数组的类型。

4、不正确的使用 Overloading

TypeScript 本身支持重载。这很好,因为它可以提高我们的可读性。但是,它不同于其他类型的重载语言。

在某些情况下,它可能会使我们的代码更加复杂和冗长。为了防止这种情况发生,我们需要牢记两条规则:

1. 避免编写多个仅尾随参数不同的重载

// ❌ instead of this
interface Example {
  foo(one: number): number;
  foo(one: number, two: number): number;
  foo(one: number, two: number, three: number): number;
}


// ❎ do this
interface Example {
  foo(one?: number, two?: number, three?: number): number;
}

你可以看到两个接口是如何相等的,但第一个比第二个更冗长。在这种情况下最好使用可选参数。

2. 避免仅在一种参数类型中编写因类型不同而不同的重载

// ❌ instead of this
interface Example {
  foo(one: number): number;
  foo(one: number | string): number;
}


// ❎ do this
interface Example {
  foo(one: number | string): number;
}

与前面的示例一样,第一个界面变得非常冗长。最好使用联合来代替。

5、使用函数类型

TypeScript 附带 Function 类型。这就像使用 any 关键字但仅用于函数。遗憾的是,启用严格模式不会阻止我们使用它。

这里有一点关于函数类型:

  • 它接受任意数量和类型的参数。

  • 返回类型始终为 any。

让我们看一个例子:

// ❌ Avoid, parameters types and length are unknown. Return type is any
const onSubmit = (callback: Function) => callback(1, 2, 3);


// ✅ Preferred, the arguments and return type of callback is now clear
const onSubmit = (callback: () => Promise<unknown>) => callback();

在上面的代码中,通过使用显式函数定义,我们的回调函数更具可读性和类型安全性。

6、依赖第三方实现不变性

在使用函数式编程范式时,TypeScript 可以提供很大帮助。它提供了所有必要的工具来确保我们不会改变我们的对象。我们不需要在我们的代码库中添加像 ImmutableJS这样的笨重的库。

让我们通过以下示例来看看我们可以使用的一些工具:

// ✅ declare properties as readonly
interface Person {
  readonly name: string;
  readonly age: number;
}


// ✅ implicitely declaring a readonly arrays
const x = [1,2,3,4,5] as const;


// ✅ explicitely declaring a readonly array
const y: ReadonlyArray<{ x: number, y: number}> = [ {x: 1, y: 1}]


interface Address {
  street: string;
  city: string;
}


// ✅ converting all the type properties to readonly
type ReadonlyAddress = Readonly<Address>;

正如你从上面的例子中看到的,我们有很多工具来保护我们的对象免于变异。

通过使用内置功能,我们将保持我们的 bundle light 和我们的类型一致。

7、不理解 infer/never 关键字

infer 和 never 关键字很方便,可以在许多情况下提供帮助,例如:

推断

使用 infer 关键字就像告诉 TypeScript,“我想把你在这个位置推断出来的任何东西分配给一个新的类型变量。”

我们来看一个例子:

const array: number[] = [1,2,3,4];


type X = typeof array extends (infer U)[] ? U : never;

在上面的代码中,作为array extends infer U[],X变量将等于 a Number。

never

该never类型表示值是不会发生的类型。

我们来看一个例子:

interface HttpResponse<T, V> {
  data: T;
  included?: V;
}


type StringHttpResponse = HttpResponse<string, never>;


// ❌ included prop is not assignable
const fails: StringHttpResponse = {
  data: 'test',
  included: {}
  // ^^^^^
  // Type '{}' is not assignable to type 'never'
}


// ✅ included is not assigned
const works: StringHttpResponse = {
  data: 'test',
}

在上面的代码中,我们可以使用 never 类型来表示我们不希望某个属性是可赋值的。

我们可以将 Omit Mapped 类型用于相同的目的:

type StringHttpResponse = Omit<HttpResponse<string, unkown>, 'included'>;

但是,你可以看到它的缺点。它更冗长。如果你检查 Omit 的内部结构,它会使用 Exclude,而后者又使用 never 类型。

通过依赖 infer 和 never 关键字,我们省去了复制任何类型的麻烦,并更好地表达我们的接口。

总结

这些指南易于遵循,旨在帮助你接受 TypeScript,而不是与之抗争。TypeScript 旨在帮助你构建更好的代码库,而不是妨碍你。

通过应用这些简单的技巧,你将拥有一个更好、更简洁且易于维护的代码库。

我们是否遗漏了你项目中经常发生的任何常见错误?请在评论中与我分享它们。

感谢你的阅读。

学习更多技能

请点击下方公众号

TypeScriptJavaScript的一个超集,增加了静态类型系统和其他面向对象编程特性的支持。以下是TypeScript的一些常见语法特性: 1. **声明变量与类型注解**: TypeScript引入了`let`、`const`和`var`之外的`type`关键字来声明类型,比如: ```typescript let name: string; // 声明一个字符串类型的变量 const age: number = 25; // 定义一个常量并指定其类型 type Person = { name: string, age: number }; // 定义一个接口表示一个人 ``` 2. **数组和元组**: 类似JavaScript,但可以明确指定元素类型: ```typescript let numbers: number[] = [1, 2, 3]; // 数组类型 let point: [number, number] = [0, 0]; // 元组类型 ``` 3. **接口(Interface)**: 描述对象结构,可用于类型安全地强制数据一致性: ```typescript interface PersonInterface { firstName: string; lastName: string; } ``` 4. **类(Class)**: 和JavaScript的构造函数相似,但有更丰富的继承和方法封装机制: ```typescript class Car { make: string; model: string; constructor(make: string, model: string) { this.make = make; this.model = model; } drive() { console.log(`Driving a ${this.make} ${this.model}`); } } ``` 5. **泛型(Generics)**: 提供了一种重用代码的方式,参数化类型: ```typescript function identity<T>(value: T): T { return value; } ``` 6. **类型断言(Type Assertion)**: 当你知道某个值的具体类型时,可以用作类型提示: ```typescript let obj: any = { prop: 'hello' }; if (obj instanceof Object) { // 这里使用Type Assertion let strProp: string = obj.prop; } ``` 7. **类型守卫(Type Guard)**: 类似条件判断,但会进行类型验证: ```typescript function isNumber(value: unknown): value is number { return typeof value === 'number'; } let maybeNum = 5; if (isNumber(maybeNum)) { console.log(`${maybeNum} is a number`); } ``` **相关问题--:** 1. 泛型在TypeScript中有哪些应用场景? 2. TypeScript如何处理JavaScript中的动态类型? 3. 类与构造函数在TypeScript中的区别是什么?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值