TypeScript是微软开发的JavaScript的超集添加了静态类型系统。

我从三年前开始接触TypeScript那时候TypeScript还不算太流行Angular 2刚用TypeScript重写很多人还在观望。

但是直到最近我才觉得自己真正入门了TypeScript。这三年走了很多弯路踩了很多坑也慢慢明白了一些道理。

今天想分享一下我用了三年TypeScript才明白的一些道理希望能帮大家少走弯路更快入门TypeScript。

一、最开始的误解

最开始接触TypeScript的时候,我对它有很多误解。

误解1:TypeScript就是加了类型的JavaScript

最开始以为TypeScript就是给JavaScript加了类型注解其他都一样。只要给变量加个类型就完事了。

后来才明白TypeScript不只是加了类型它有一套完整的类型系统包括接口泛型枚举类型守卫高级类型等等能做很多JavaScript做不到的事情。

而且TypeScript还支持最新的JavaScript特性甚至一些还在,提案阶段的特性能让我们提前使用新的语法。

误解2:类型是负担增加开发时间

最开始觉得写类型是负担要多写很多代码增加开发时间。而且JavaScript动态类型用习惯了觉得类型限制了灵活性。

后来才明白类型不是负担而是帮助。类型能在编译阶段就发现很多错误不用等到运行的时候,才发现。而且类型是最好的文档看类型就知道函数的参数和返回值是什么不用去看实现代码。

而且有了类型IDE的智能提示会非常强大自动补全跳转定义重构都很方便能大大提高开发效率。

长期来看写类型的时间会从减少debug时间和提高维护效率中赚回来。

误解3:any类型走天下

最开始写TypeScript的时候,遇到类型不好写的地方就用any一用any万事大吉类型检查都绕过了。

后来才明白滥用any就失去了TypeScript的意义和,写JavaScript没有区别。而且any会让类型系统失效错误不能在编译阶段发现等到运行的时候,才报错就晚了。

应该尽量避免使用any除非真的不知道类型,或者在快速原型阶段。可以用unknown代替anyunknown是类型安全的需要做类型检查才能使用。

二、明白的道理

用了三年才慢慢明白以下这些道理。

道理1:类型系统是TypeScript的核心

TypeScript的核心是它的类型系统而不是其他的特性。理解了类型系统才算真正入门了TypeScript。

TypeScript的类型系统是结构化的类型系统也叫鸭子类型。也就是说,只要两个类型的结构一样它们就是兼容的,不管名字是否一样。

比如:

interface Point {
  x: number;
  y: number;
}

function logPoint(p: Point) {
  console.log(p.x, p.y);
}

const point = { x: 1, y: 2, z: 3 };
logPoint(point); // OK因为point有x和y结构兼容

这里point虽然没有声明为Point类型,但是它有x和y属性结构和Point兼容,所以可以传给logPoint。

理解了结构化类型系统才能理解TypeScript的类型兼容规则。

道理2:接口和类型别名的区别

最开始分不清interface和type的区别觉得它们一样随便用。

后来才明白它们有区别各有适用场景。

interface只能定义对象类型支持声明合并也就是多个同名的interface会自动合并。而且interface支持继承用extends关键字。

type可以定义任何类型包括基本类型联合类型交叉类型元组等等更灵活。但是type不支持声明合并。

一般来说,定义对象的形状用interface因为支持继承和声明合并更符合面向对象的习惯。定义联合类型交叉类型基本类型别名等用type更灵活。

比如:

// 用interface定义对象形状
interface User {
  id: number;
  name: string;
}

interface Admin extends User {
  role: string;
}

// 用type定义联合类型和别名
type Status = 'active' | 'inactive' | 'pending';
type ID = number | string;
type UserWithStatus = User & { status: Status };

道理3:泛型是TypeScript的精髓

泛型是TypeScript最强大也最难的部分也是TypeScript的精髓。

最开始觉得泛型很抽象不好理解也不怎么用。后来才明白泛型能让代码更通用更类型安全是写高质量TypeScript代码的关键。

泛型允许我们在定义函数接口类的时候,不指定具体的类型而是用一个类型参数代替在使用的时候,再指定具体的类型。

比如一个通用的identity函数:

function identity<T>(arg: T): T {
  return arg;
}

const num = identity<number>(1); // number
const str = identity<string>('hello'); // string

这里T就是类型参数调用的时候,指定具体的类型。

泛型还支持约束用extends关键字限制类型参数的范围:

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength('hello'); // OKstring有length
logLength([1, 2, 3]); // OK数组有length
logLength(1); // Errornumber没有length

掌握了泛型才能写出真正通用类型安全的代码。

道理4:类型守卫能缩窄类型

TypeScript有一个很重要的特性叫类型守卫能在条件判断中缩窄类型。

最开始不知道类型守卫遇到联合类型的变量要访问某个类型才有的属性就报错不知道怎么处理。

后来才明白用类型守卫能缩窄类型让TypeScript知道在某个分支中变量是什么类型。

常见的类型守卫有:

  • typeof:判断基本类型,比如typeof x === 'string'
  • instanceof:判断实例类型,比如x instanceof Date
  • in:判断属性是否存在,比如'name' in x
  • 自定义类型守卫:用is关键字定义自己的类型守卫

比如:

interface Cat {
  meow(): void;
}

interface Dog {
  bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

function speak(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // 这里TypeScript知道animal是Cat
  } else {
    animal.bark(); // 这里TypeScript知道animal是Dog
  }
}

掌握了类型守卫才能灵活处理联合类型的变量。

道理5:高级类型让类型系统更强大

TypeScript有很多高级类型,比如交叉类型联合类型条件类型映射类型索引类型等等能让类型系统更强大。

最开始不知道这些高级类型后来慢慢学习才发现它们非常有用能实现很多复杂的类型操作。

比如条件类型能根据条件选择不同的类型:

type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<123>; // false

映射类型能从一个类型映射出另一个类型:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

interface User {
  id: number;
  name: string;
}

type ReadonlyUser = Readonly<User>; // { readonly id: number; readonly name: string; }

TypeScript内置了很多工具类型,比如PartialRequiredReadonlyPickOmitRecordReturnType等等都是用高级类型实现的非常有用。

掌握了高级类型才能真正发挥TypeScript类型系统的威力。

道理6:严格模式很重要

TypeScript有严格模式在tsconfig.json中设置"strict": true会开启所有严格类型检查选项。

最开始觉得严格模式太严格了很多代码都报错不方便就没开。

后来才明白严格模式很重要能帮我们发现更多潜在的问题写出更健壮的代码。

严格模式包括这些选项:

  • noImplicitAny:不允许隐式的any类型
  • strictNullChecks:严格检查null和,undefined
  • strictFunctionTypes:严格检查函数类型
  • strictBindCallApply:严格检查bindcallapply
  • strictPropertyInitialization:严格检查属性初始化
  • alwaysStrict:总是使用严格模式

特别是strictNullChecks能避免很多null引用错误这是,JavaScript中最常见的错误之一。

建议所有TypeScript项目都开启严格模式,虽然一开始会不习惯,但是长期来看能大大提高代码质量。

道理7:类型声明文件很重要

TypeScript需要类型声明文件(.d.ts)来知道第三方库的类型。

最开始用第三方库的时候,不知道要安装类型声明文件就报错找不到类型不知道怎么处理。

后来才明白很多第三方库本身不带类型声明需要单独安装@types开头的类型包,比如@types/node@types/lodash@types/react等等。

而且TypeScript社区维护了DefinitelyTyped项目提供了几千个库的类型声明文件非常方便。

如果某个库没有类型声明文件可以自己写一个declare module声明先用any类型顶着后面再完善。

道理8:不要过度设计类型

虽然TypeScript的类型系统很强大,但是也不要过度设计类型。

最开始学会了泛型高级类型之后,就想到处用把类型写得很复杂很"优雅"但是反而降低了代码的可读性和可维护性。

后来才明白类型是为代码服务的不是为了炫技。类型应该简单清晰易读不要为了类型而类型。

如果一个类型写得很复杂别人看半天都看不懂那可能就是过度设计了应该简化,或者用更简单的方式。

而且有些场景用any或者类型断言反而更简单更清晰不要为了追求100%类型安全把代码写得很复杂。

三、最佳实践

最后总结一下TypeScript的一些最佳实践。

1. 开启严格模式

所有项目都应该开启严格模式提高代码质量。

2. 避免使用any

尽量避免使用any用unknown代替,或者写更精确的类型。

3. 合理使用interface和type

对象形状用interface联合类型等用type合理选择。

4. 善用泛型

需要通用代码的时候,用泛型提高代码复用性和类型安全。

5. 使用工具类型

善用TypeScript内置的工具类型,比如PartialPickOmitRecord等等减少重复代码。

6. 写好类型声明

第三方库没有类型的时候,自己写类型声明提高开发体验。

7. 不要过度设计

类型要简单清晰易读不要过度设计为了类型而类型。

8. 善用IDE

TypeScript和IDE(VS Code)配合非常好善用智能提示跳转定义重构等功能提高开发效率。

四、写在最后

以上就是我用了三年TypeScript才明白的一些道理。

TypeScript是一个很优秀的语言它给JavaScript加上了静态类型系统能帮我们写出更健壮更可维护的代码。

但是TypeScript也有学习曲线特别是它的类型系统比较复杂需要时间去理解和,掌握。我用了三年才觉得自己真正入门了走了很多弯路。

希望我的这些经验能帮大家少走弯路更快入门TypeScript享受类型系统带来的好处。

当然TypeScript还在快速发展新的特性不断加入我也还在不断学习和探索。

最后用一句话结束这篇文章:"类型不是束缚而是自由它让我们在安全的前提下更自由地写代码。"

愿大家都能用好TypeScript写出高质量的代码。