如何在项目中用好TypeScript

写在最前:本文转自掘金

1. 前言

  • 我们都知,JavaScript 是一门非常灵活的编程语言,这种灵活也使他的代码质量参差不齐,维护成本高,运行时错误多。
  • TypeScript是添加了类型系统的JavaScript,适用于任何规模的项目,TS的类型系统很大程度上弥补了JS的缺点。
  • 类型系统按照[类型检查的时机]来分类,可以分为动态类型和静态类型:
    · 动态类型是指运行时才会进行类型检查,这种语言的类型错误往往会导致运行时的错误,JS属于动态类型,它是一门解释型语言,没有编译阶段。
    · 静态类型是只编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。由于TS在运行前需要先编译成JS,而在编译阶段就会进行类型检查,所以TS属于静态类型。
  • TS增强了编辑器的功能,包括代码补全、接口提示、跳转到定义、代码重构等,这在很大程度上提高了开发效率。TS的类型系统可以为大型项目带来更高的可维护性,以及更少的bug。
  • 为了提升开发幸福感,下面将详细介绍如何在项目中用好TS。

2. 在项目中的实践

2.1 善用类型注释

  • 我们可以通过/** */ 形式的注释给TS类型做标记提示:
/** person information**/
interface User{
  name: string;
  age: number;
  sex: 'male' | 'female' ;
}

const p:User = {
  name: "Lucky",
  age:20,
  sex:"female"
}

当鼠标悬浮在使用到该类型的地方时,编辑器会有更好的提示:


test1.png

2.2 善用类型扩展

  • TS 中定义类型有两种方式:接口(interface)和类型别名(type alias)。在下面的例子中,除了语法不一样,定义的类型是一样的:
// interface
interface Point{
  x: number;
  y: number;
}
interface SetPoint{
  (x: number, y: number): void;
}

// type
type Point = {
  x: number;
  y: number;
}
type SetPoint = (x: number, y: number)=> void;
  • 接口和类型别名均可以扩展:
// Interface extends interface
interface PartialPointX{
  x: number;
}
interface Point extends PartialPointX{
  y: number;
}

// Type alias extends type alias
type PartialPointX={
  x:number;
}
type Point = PartialPointX & {y:number;};
  • 接口和类型别名并不互斥的,也就是说,接口可以扩展类型别名,类型别名也可以扩展接口:
// Interface extends type alias
type PartialPointX={
  x: number;
}
interface Point extends PartialPointX{
  y: number;
}

// Type alias extends interface
interface PartialPointX{
  x:number;
}
type Point = PartialPointX & {y:number;};
  • 接口和类型别名的选用时机
    · 在定义公共API(如编辑一个库)时使用interface,这样可以方便使用者继承接口;
    · 在定义组件属性(Props)和状态(State)时,建议使用type,因为type的约束性更强;
    · type类型不能二次编辑,而interface可以随时扩展。

2.3 善用声明文件

  • 声明文件必须以.d.ts为后缀。一般来说,TS会解析项目中所有的*.ts文件,因此也包含以.d.ts结尾的声明文件。
  • 只要ts.config.json中的配置包含了typing.d.ts文件,那么其他的所有*.ts文件就都可以获得声明文件的类型定义。
2.3.1 第三方声明文件
  • 当在TS项目中使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
  • 针对多数第三方库,社区已经帮我们定义好了它们的声明文件,我们可以直接下载下来使用。一般推荐使用@types 统一管理第三方库的声明文件,@types的使用非常简单,直接用npmyarn安装对应的声明模块即可。以lodash为例:
npm install @types/lodash --save-dev
// or
yarn add @types/lodash --dev
2.3.2 自定义声明文件
  • 当一个库没有提供声明文件,就需要我们自己写声明文件,以antd-dayjs-webpack-plugin为例,当在config.ts中使用antd-dayjs-webpack-plugin时,若当编辑器没有找到它的声明文件,则会发生报错
  • 为了解决编辑器的报错提示,我们可以采用它提供的另一种方法:添加一个包含 declare module 'antd-dayjs-webpack-plugin'; 的新声明文件。我们也可以不用新增文件,在前面提到的 typing.d.ts 添加下面的内容即可:
declare module 'antd-dayjs-webpack-plugin';

全局变量

当我们需要在多个ts文件中使用同一个typescript类型时,常见做法会在constant.ts文件中声明相关类型,并将其export出去给其他ts文件import使用,无疑会产生很多繁琐的代码。前面我们提到,只要在tsconfig.json中配置包含了我们自定义的声明文件*.d.ts,则声明文件中的类型都能被项目中.ts文件获取到。因此我们可以将多个ts文件都需要使用的全局类型卸载声明文件中,需要使用该类型的ts文件不需要import就可以直接使用。

命名空间

在代码量较大的情况下,为了避免各种变量名冲突,可将相同模块的函数、类、接口等放置在命名空间内。


test2.jpeg

在ts文件使用:

// src/views/Domain/index.ts
const cloumns: Domains.ListItem[] = []
...

// src/views/Department/index.ts
const columns: Departments.ListItem[] = []

2.4 善用 TypeScript 支持的JS新特性

2.4.1 可选链
let  age = user && user.info && user.info.getAge  // 写法冗余且容易命中 `Uncaught TypeError: Cannot read property ...`

let age = user?.info?.getAge  //如果其中有属性不存在,会返回`null`或者`undefined`
2.4.2 空值合并运算符

当左侧的操作数为null或者undefined时,返回其右侧操作数,否则返回左侧操作数。

const user = {
  level: 0, 
}
let level1 = user.level ?? '暂无等级'  // 0
let level2 = user.other_level ?? ''暂无等级'  // 暂无等级

||不同,或操作符为false值(例如,' '0)时返回右侧操作数

2.5 善用访问限定

TS的类定义时允许使用privateprotectedpublic三种访问修饰符声明成员访问限制,并在编译期间检查,如果不加任何修饰符,默认为public访问级别:

class Person {
  private name: string;
  private age: number;
  // static 关键字可以将类里面的属性和方法定义为类的静态属性和方法
  public static sex: string = 'Male';
  constructor(name: string, age: number){
    this.name = name;
    this.age = age;
  }
  public run(): void {
    console.log(this.name + '在跑步')
  }
  public setName(name:string): void {
    this.name = name;
  }
}
let p: Person = new Person('Tony', 22);
console.log(Person.sex);  // Male
p.run();  //Tony在跑步
console.log(p.name) // name为私有属性

2.6 善用类型收窄

TypeScript 类型收窄就是从宽类型转换成窄类型的过程,其常用于处理联合类型变量的场景。主要有以下方法收窄变量类型:

  • 类型断言
  • 类型守卫
  • 双重断言
2.6.1 类型断言

类型断言可以明确地告诉TS值的详细类型。其语法如下:

值 as 类型
// or
<类型>值

在 tsx 语法中必须使用前者。当TS不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性和方法。

如何不完整的去实现接口结构
例如,有一下接口,并被obj变量实现,但赋值空对象会报错

interface IStruct {
  foo: string;
  bar: {
    barPropA: string;
    barPropB: number;
    barMethod: () => void;
    baz: {
      handler: () => Promise;
    };
  };
}

const obj: IStruct = {}  // 报错

这个还是就可以使用类型断言帮助实现先定义,后赋值,同时类型提示仍然存在:

const obj = {
}

需要注意的是,类型断言只能够欺骗TS编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误

2.6.2 类型守卫

类型守卫主要有以下几种方式:

  • typeof :用于判断numberstringbooleansymbol四种类型;
  • instanceof:用于判断一个实例是否属于某个类;
  • in:用于判断一个属性/方法是否属于某个对象

typeof

可以利用 typeof 实现类型收窄和never 类型的特性做全面性检查,如下面代码所示:

type Foo = string | number

function controlFlowAnalysisWithNever(foo: Foo){
  if(typeof foo === 'string'){
    // 这里foo收窄为 string类型
  }else if (typeof foo === 'number'){
    // 这里foo 被收窄为number类型
  } else {
    // foo 在这里是never
    const check: never = foo;
  }
}

可以看到,在最后的else分支里面,我们把收窄为neverfoo赋值给一个显式声明的never变量,如果一切逻辑正确,那么是能够编译通过。假如某天某人修改了Foo的类型,而忘记修改controlFlowAnalysisWithNever方法中的控制流程,这时候else分支的foo类型无法赋值给never类型,产生一个编译错误。通过使用never避免出现新增了联合类型没有对应的实现,确保了方法总是穷尽Foo的所有类型,从而保证代码的安全性。

但如果我们将typeof判断逻辑提取到函数外部进行复用

function isString(input: unknown): boolean {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 类型“string | number”上不存在属性“replace”。
    (input).replace("linbudu", "linbudu599")
  }
  if (typeof input === 'number') { }
  // ...
}

奇怪的事情发生了,如果 isString 返回了 true ,那input肯定是string类型。但因为TS 无法做到跨函数上下文来进行类型的信息收集判断。为了弥补该项的不足,TS引入了is关键字来显式地提供类型信息:

function isString(input: unknown): input is string {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 正确了
    (input).replace("linbudu", "linbudu599")
  }
  if (typeof input === 'number') { }
  // ...
}

isString 函数称为类型守卫,在它的返回值中,我们不再使用 boolean 作为类型标注,而是使用 input is string 这么个奇怪的搭配,拆开来看它是这样的:

  • input 函数的某个参数;
  • is string,即 is 关键字 + 预期类型,即如果这个函数成功返回为 true,那么 is 关键字前这个入参的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到

注意,类型守卫函数中并不会对判断逻辑和实际类型的关联进行检查:

// 只会显式的认为 input is number
function isString(input: unknown): input is number {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 报错,在这里变成了 number 类型
    (input).replace("linbudu", "linbudu599")
  }
  if (typeof input === 'number') { }
  // ...
}

从这个角度来看,其实类型守卫有些像类型断言,但是类型守卫更宽容,更信任你一些。你指定什么类型,它就是什么类型。
这里提供开发中两个常用的两个类型守卫:

export type Falsy = false | "" | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;

export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean' , 'undefined'].includes(typeof val);

instanceof

使用instanceof运算符收窄变量的类型:

class Man {
  handsome = "handsome";
}
class Women{
  beautiful = "beautiful"
}

function Human(arg: Man | Woman){
  if(arg instanceof Man){
    console.log(arg.handsome)
  }else{
    console.log(arg.beautiful)
  }
}

in

使用in做属性检查

interface A{
  a: string; 
}
interface B{
  b: string; 
}
function foo(x: A | B){
  if("a" in x){
    return x.a;
  }
  return x.b
}
2.6.3 双重断言

当我们要为某个值作类型断言时,我们需要确保编辑器推断出的值的类型和新类型有重叠,否则,无法简单地作类型断言,任何类型都可以被断言为any,而any可以被断言为任何类型
如果我们仍然想使用那个类型,可以使用双重断言

function handler(event: Event){
  const element = event as any as HTMLElement;
}

TS3.0中新增了一种unknown类型,它是一种更加安全的any的副本。所有东西都可以被标记成是unknown类型,但是unkonwn必须在进行类型判断和条件控制之后才可以被其他类型,并且在类型判断和条件控制之前也不能进行任何操作

2.7 善用常量枚举

2.8 善用高级类型

除了stringnumberboolean这种基础类型外,我们还应该了解一些类型声明中的一些高级用法。

2.8.1 类型索引(keyof)

keyof 类似于Object.keys,用于获取一个接口中key的联合类型:

interface Button{
  type: string;
  text: string;
}
type ButtonKeys = keyof Button
// 等效于
type ButtonKeys = "type" | "text"
2.8.2 类型约束(extends)

TSextends关键词不同于在Class后使用extends的继承作用,一般在泛型内使用,它主要作用是对泛型加以约束:

type BaseType = string | number | boolean  // 这里表示copy 的参数

function copy(arg: T):T{
  return arg
}
const arr = copy([])  // error

extends经常和keyof一起使用,例如我们有一个getValue方法专门获取对象的值,但是这个对象并不确定,我们就可以这样做:

function getValue(obj: T, key: K){
  return obj[key]
}

const obj = {a:1}
const a = getValue(obj, 'b')  //error

当传入对象没有key时,编辑器则会报错

2.8.3 类型映射(in)

in关键词的作用主要是做类型的映射,遍历已有接口的key或者是遍历联合类型。以内置的泛型接口Readonly为例,它的实现如下:

type Readonly ={
  readonly [P in keyof T]: T[P];
} 
// 它的作用是将所有接口变为只读
interface Obj {
  a: string;
  b: string;
}
type ReadOnlyObj = Readonly
//等效于
interface Obj {
  readonly a: string;
  readonly b: string;
}
2.8.3 条件类型(U?X:Y)

条件类型的语法规则和三元表达式一致,经常用于类型不确定的情况

T extends U ? X : Y

上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y。以内置的泛型接口 Extract 为例,它的实现如下:

type Extract = T extends U ? T : never;

TypeScript 将使用 never类型来表示不应该存在的状态。上面的意思是,如果 T 中的类型在 U 存在,则返回,否则抛弃。
假设我们两个类,有三个公共的属性,可以通过Extract提取这三个公共属性:

interface Worker{
  name: string;
  age: number;
  email:string;
  salary: number;
}
interface Worker{
  name: string;
  age: number;
  email:string;
  grade: number;
}

type CommonKeys = Extract
// 'name' | 'age' | 'email'
2.8.4工具泛型

TS 中内置了很多工具泛型,前面介绍过Readonly、'Extract' 这两种,内置的泛型在TS内置的lib.es5.d.ts中都有定义,所以不需要任何依赖就可以直接使用。下面介绍几个常见的工具泛型的作用和使用方法。
Exclude,如果T中的类型在U不存爱,则返回,否则抛弃。

interface Worker{
  name: string;
  age: number;
  email:string;
  salary: number;
}
interface Worker{
  name: string;
  age: number;
  email:string;
  grade: number;
}

type CommonKeys = Exclude
// 'salary' 

Partial 用于将一个接口所有属性设置为可选状态:

interface Person  {
  name: string;
  sex: string;
}
type NewPerson = Partial
// {name?:string;sex?:string}

Required的作用是将所有接口可选属性改为必选的

interface Person  {
  name?: string;
  sex?: string;
}
type NewPerson = Required
// {name:string;sex:string}

Pick主要作用提取接口的某几个属性:

interface Todo{
  title: string;
  completed: boolean;
  description: string
}
type TodePrevied = Pick
// {title: string;completed:boolean}

Omit的作用是剔除接口的某几个属性

interface Todo{
  title: string;
  completed: boolean;
  description: string
}
type TodePrevied = Omit
// {title: string;completed:boolean}
2.8.5 工具泛型获取组件实例的类型

InstanceType 是 ts 自带的类型, 能够直接获取组件完整的实例类型

  import Child from './child.vue'
  import {ElImage} from 'element-plus'
  
  type ElImageCtx = InstanceType(typeof ElImage);
  type ChildCtx = InstanceType(typeof Child);
  
  ...
  
  setup() {
     
     const child = ref(null);
     const elImgRef = ref(null)
     
     onMounted(() => {
       child.value?.num ;// 可以直接访问到
       elImgRef.value?. // 对于 element组件,可以访问到很多的属性
     })
  
  }

你可能感兴趣的:(如何在项目中用好TypeScript)