TypeScript技术系列14之最终章:常见的TS错误汇总

167 阅读6分钟

前言

在实际项目中使用TypeScript时,开发者往往会遇到各种类型错误。虽然TypeScript官方文档提供了详尽的类型系统指南,但现实开发场景中,仍然存在大量鲜有提及的类型错误,极易被忽略或误解,尤其是在涉及泛型、高级类型、枚举及类型推导时。这篇文章将系统性地梳理几个常见但不容易察觉的TypeScript类型错误,并介绍如何为类型编写单元测试,确保类型逻辑的正确性。

1、类型错误详解

1.1 TS2456:类型别名的递归引用

在使用类型别名定义递归数据结构时,如果没有妥善设置终止条件,可能会触发类型展开无限循环的问题。例如:

// 错误示例
// TS2456: Type alias 'Node' circularly references itself.
type Node = { next: Node };

这是由于TypeScript在推导Node的类型时,必须无限递归解析其结构,导致类型系统进入死循环。要解决这个问题,可以通过引入nullundefined明确终止条件:

// 正确示例
type Node = { value: number; next: Node | null };

这种处理方式常见于链表、树形结构等场景,是构建复杂递归类型的关键技巧。

1.2 TS2554:实参与形参数量不一致

这个错误在使用函数调用时较为常见。例如:

function sum(a: number, b: number): number {
  return a + b;
}

sum(1); // TS2554: Expected 2 arguments, but got 1.

即使参数bundefinedTypeScript也认为你没有传入必需的参数。解决方式可以使用可选参数:

function sum(a: number, b?: number): number {
  return b === undefined ? a : a + b;
}

或使用默认参数:

function sum(a: number, b: number = 0): number {
  return a + b;
}

1.3 TS1169:接口中使用非法属性名

接口支持的属性名仅限于字面量类型或unique symbol类型。如果使用了非字面量类型作为属性名,将触发TS1169错误:

type Keys = string | number;

// TS1169: A computed property name in an interface must refer to a literal type or a 'unique symbol' type.
interface Bad {
  [key in Keys]: any;
}

正确的做法应使用类型别名或映射类型声明对象结构:

type Good = {
  [key in 'id' | 'name']: string;
};

或改用索引签名:

interface Flexible {
  [key: string]: any;
}

1.4 TS2345:参数类型不兼容

这种错误多见于传入枚举、联合类型、函数参数类型不匹配时。例如:

enum StatusA {
  Success = 'SUCCESS',
  Fail = 'FAIL',
}

enum StatusB {
  Success = 'SUCCESS',
  Fail = 'FAIL',
}

function handleStatus(status: StatusA) {}

handleStatus(StatusB.Success); // TS2345

尽管两个枚举值字符串相同,但它们属于不同的枚举类型。解决方式是使用类型断言:

handleStatus(StatusB.Success as unknown as StatusA);

或考虑使用联合字面量类型而不是枚举:

type Status = 'SUCCESS' | 'FAIL';
function handleStatus(status: Status) {}

1.5 TS2589:类型实例化过深

当使用泛型构建递归类型时,超过TypeScript最大递归深度(默认为50)时,会触发TS2589错误。例如:

type Repeat<T, N extends number, R extends T[] = []> =
  R['length'] extends N ? R : Repeat<T, N, [...R, T]>;

type TenStrings = Repeat<'x', 10>; // OK
type TooMany = Repeat<'x', 100>; // TS2589

为避免这类问题,可以控制递归层数,或在必要时使用@ts-ignore忽略错误。

1.6 TS2322:字面量类型不匹配

当将对象赋值给具备字面量约束的类型时,TypeScript会自动将对象属性推导为宽泛类型。例如:

interface ButtonProps {
  variant: 'primary' | 'secondary';
}

const props = {
  variant: 'primary',
};

const config: ButtonProps = props; // TS2322

因为props.variant被推导为string而不是'primary',可以使用断言或类型注解解决:

// 方法一
const props: ButtonProps = {
  variant: 'primary',
};

// 方法二
const props = {
  variant: 'primary' as 'primary',
};

1.7 TS2532:类型收缩失效

在闭包或异步函数中,即使变量已在外层进行类型收缩,TypeScript在内部作用域仍不会继承缩小后的类型。例如:

let message: string | undefined;

if (message) {
  setTimeout(() => {
    message.trim(); // TS2532
  });
}

TypeScript无法确定message在异步执行时仍为string类型。解决方法是复制局部变量:

if (message) {
  const m = message;
  setTimeout(() => {
    m.trim(); // OK
  });
}

或在函数内部再次进行判断。

2、类型系统的单元测试

传统单元测试主要验证函数返回值是否符合预期。但TypeScript类型本身在运行时会被擦除,如何验证类型逻辑的正确性呢?有以下几种常见做法:

2.1 利用类型约束进行测试

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

type Test1 = Expect<Equal<1, 1>>; // OK
type Test2 = Expect<Equal<1, 2>>; // TS2322

2.2 使用 @ts-expect-error 测试异常情况

// @ts-expect-error
const num: number = 'wrong type';

// 如果上面没有报错,将提示 TS2578(注释未生效)

2.3 借助 tsd 工具

tsd是专门用来测试TypeScript类型的工具,使用方法如下:

安装:

npm install --save-dev tsd

示例测试文件:

// test/index.test-d.ts
import { isEqual } from '../src/utils';
import { expectType, expectError } from 'tsd';

expectType<boolean>(isEqual(1, 2));
expectError(isEqual(1));

运行测试:

npx tsd

2.4 使用conditional类型断言逻辑

可以为自定义类型工具函数编写一套类型层的测试框架:

type IsNever<T> = [T] extends [never] ? true : false;
type IsAny<T> = 0 extends (1 & T) ? true : false;
type IsUnknown<T> = unknown extends T ? ([keyof T] extends [never] ? true : false) : false;

// 测试
type T1 = Expect<Equal<IsNever<never>, true>>;
type T2 = Expect<Equal<IsAny<any>, true>>;
type T3 = Expect<Equal<IsUnknown<unknown>, true>>;

这类类型测试方案可以有效辅助开发复杂类型工具函数。

总结

通过本篇文章,深入剖析了TypeScript在实际开发中容易遇到但官方文档较少提及的一些类型错误,并结合实际示例介绍了如何识别与修复这些问题。同时,还介绍了如何在TypeScript项目中使用类型层面的“单元测试”机制来验证工具类型是否按预期工作。这种类型测试的方式虽然不会运行在浏览器或Node.js中,但却是静态类型系统的强大补充,可以帮助开发者在类型层面提升代码的安全性和可靠性。

在这一系列《TypeScript技术系列》文章中,我们系统地介绍了TypeScript的核心语法、类型系统、泛型机制、高级类型工具、类型体操技巧,以及如何处理tsconfig.json配置项、类型兼容性与分发、常见类型错误及类型测试等内容。每一篇文章都试图在理论的基础上,通过大量示例代码帮助读者加深理解,最终将TypeScript应用于真实项目中。

如果你已经从头认真阅读了整个系列内容,那么你已经具备了相当扎实的TypeScript知识体系,可以从容面对日常开发中遇到的大多数类型问题。

至此,《TypeScript技术系列》完结。

衷心希望本系列内容能成为你在TS学习与实战路上的得力助手。感谢一路同行!!!

后语

小伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走吧^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。