7

总结TypeScript在项目开发中的应用实践体会

 3 years ago
source link: https://juejin.cn/post/6970841540776329224
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

总结TypeScript在项目开发中的应用实践体会

作者:@wangly19,本文已授权掘金开发者社区公众号独家使用,包括但不限于编辑、标注原创等权益。

2020年年底的时候,我开始使用Typescript进行项目的开发。期间团队也开始转向Typescript

在这期间,做过很多尝试,也阅读过一些优质的文章和源码。现如今,大多数开源项目都将Typescript做为开发的主力军。

在这期间,我查阅的大多数文章都是在进行一个Typescript的基础使用,开发实践这一块更是少之又少,少有的一些干货文啃起来也非常的不过瘾。

相信在读的各位收藏夹里面已经有很多份学习Typescript的小文章都在吃灰,看了一遍但到了项目中依旧无从下手,该如何去进行开发?

独乐乐不如众乐乐,本篇文章就从开发的角度来聊聊,探讨下Typescript在真实项目中开发的实践心得和开发体验。

当你看完文章时,我建议先思考团队是否需要Typescript。以及Typescript是否可以解决当前项目生产的困境。

如果对于为什么使用TypeScript产生疑惑,那么可以移步你为什么不使用 TypeScript?,它是一个非常棒的讨论话题。

必知必会的特性

TypeScript中,有一些好用的特性功能对于日常开发来说是比较常见的。下面就罗列一些较为实用的知识点作为一个小小的备忘录。

Readonly

有了Readonly,可以声明更加严谨的可读属性,亦或者变量。

ES6当中,可以通过const进行常量量声明,切声明后不可修改,如果进行修改的话会直接Cannot assign to 'a' because it is a constant.进行异常抛错。

虽然不能更改整个值,但是如果值是一个引用类型的话,依旧可以对其内部的属性进行修改。那么从只读的概念上来说,显然不具备当前的能力。

而使用Typescript当中的readonly关键字对属性或者是变量进行声明,那么将会在编译时就发出告警。那么在声明部分

image.png

条件类型(Conditional Type)

如果你不知道条件类型,那么来看一段@vue/reactivity中的代码吧。

export type DeepReadonly<T> = T extends Builtin
  ? T
  : T extends Map<infer K, infer V>
    ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
    : T extends ReadonlyMap<infer K, infer V>
      ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
      : T extends WeakMap<infer K, infer V>
          ......
复制代码

其中DeepReadonly通过extends的方式继承父类然后通过? : 表达式来进行一个类型三目运算符的操作进行一个类型的条件判断。

通过一个简单的案例来进行理解,当泛型Tstring类型的时候,那么B1,反之为2。可以看到同样的一个类型,因为传入的泛型T不一样,结果自然而然的有了出入。

image.png

namespace

命名空间(namespace)是一个比较常见的东西,它常用于组织一份类型区域防止类型之间的重命名冲突,需要配置 declare 输出到外部环境才能够使用,非常便捷的在于使用declare namespace在工程项目中可以不需要引入任何类型而直接可以访问。

image.png

declare

declare是用于声明形式存在的。

  • declare var/let/const用来声明全局的变量。
  • declare function 用来声明全局方法(函数)
  • declare class 用来声明全局类
  • declare namespace 用来声明命名空间
  • declare module 用来声明模块
  • ...

在这里需要注意的是Global augmentations have the same behavior and limits as module augmentations.

Declaration Merging

什么意思呢?

image.png

大体上翻译成大白话就是:

declaredeclare global它们功能是一样的。在d.ts中,使用declaredeclare global两个作用是相等的。

因此,在d.ts进行declare,它默认是全局的,在使用declare global显得有点画蛇添足了。

那么什么时候使用declare, 又什么时候使用declare global

在模块文件中定义declare,如果想要用作全局就可以使用declare global完成该需求。

那么,可以来看个🌰栗子,看完之后就大体上懂了,都是一些比较常见的实例。

  • 在使用TypeScript开发的时候想为一些API添加一些自定义的属性,或者进行一些覆盖。
  • 在使用vue的时候,通过import引入的vue组件大多会提示错误。

如何解决?

可以通过对模块的定义来进行.vue文件模块进行一个declare module在内部可以将其export为相关类型。在这里vue2vue3不太一样。

declare module '*.vue' {
    ///
    export ...
}
复制代码

在渐进式的过程中,很多代码和包都可能没有对应的.d.ts。因此需要对部分文件进行.d.ts的类型文件编写,那么,你真的知道ES ModuleCommonJS Module之间的导入吗?

ES Module的引入方式大家都知道,但是如何对其声明.d.ts,就看下面这个用例。

我对config/index.js创建了一个index.d.ts作为其声明文件,并且导出了config对象。那么,我如何将类型提供给引入方呢?

首先,知道index.js导出是一个对象,那么declare const一个类型出来,然后通过export = config的形式对导出进行声明。那么在通过import { xxx } from '@/config就可以获悉具体的类型了。

declare const config: BaseConfig & EnvConfig

export = config

复制代码

如图,baseApi拥有了HTTPHTTPS的类型。

模板字符串类型

模板字符串是一个非常有意思的东西,它能够对文本进行一定程度上的约束,如上面baseApi在项目中被定义为了HTTP | HTTPS的类型。约定当前值中必须包含http://或者是https://才算校验成功。

// global.d.ts
declare type HTTP = `http://${string}`
declare type HTTPS = `https://${string}`

// @/config/index.d.ts
type baseApi = HTTP | HTTPS
复制代码

同样的,在使用dva中,也可以利用特性对type进行namespaceaction的组合,这样在写dispatch时,可以有一定的提示和约束能力。

想看更多实践可以看sshTypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?这篇文章深入一下。

函数重载是一个非常常用的特性,它大多数用于多态函数。大多数同学可能都不怎么使用。但是它能够定义不同的参数类型。需要有多个重载签名和一个实现签名

  • 重载签名:就是对参数形式的不同书写,可以定义多种模式。
  • 实现签名:对函数内部方法的具体实现。

getter/setter

get/set存取器是在class当中比较实用的一个功能,它保证了类中变量的私有化。在外部时时不能直接对其更改的,如果大家了解javaBean的话理解起来并不是很困难。

class中声明一个带_下标的变量,那么就可以通过getset对其进行设置值。

在实例中当我们调用.name的时候,其实本身就是调用了其get的方式,而设置值时,则是调用set方法,

需要注意的是,._name值也输出了,但是TypeScript会进行提示你._name是私有的不允许你访问。

对于typescript思想来说,(enum)是对代码具有侵入式的,它的实现方式可以看其编译成javascript后的代码。

枚举(enum)的使用场景在于可以定义部分行为和状态。通过一个🌰可以来看下:

对其某个任务的行为定义在枚举当中,这样做可以进行一些状态复用,避免在页面写太多status === 1的代码,因为没人知道1代表什么,有什么含义,不利于维护。

将其定义成enum的标注用于标识状态,如:status === Status.START

枚举可以看一篇阿宝哥的小文章一文让你彻底掌握 TS 枚举

泛型是TypeScript当中必知必会的一个属性,在很多的时候,类型推导在开始时很难进行推倒。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

泛型很多时候作用于对一个类型的多种形态定义,能够非常灵活的对一个类型进行定义和延伸推导。

那么,来看几个比较简单的实例

简单的泛型

type Generics<T> = {
    name: string
    age: number
    sex: T
}

interface Generics<T> {
    name: string
    age: number
    sex: T
}
复制代码

简单的函数泛型

function setSex<T> (sex: T) {
}

setSex<'男'>('女')
复制代码
class Person<T> {
    private sex: T;
    constructor(readonly type: T) { 
        this.sex = type; 
    }
}

const person = new Person<'男'>('女')
复制代码

对于泛型的实践来说,使用是需要一定理解,复杂的泛型使用会非常的复杂。

TypeScript当中也提供了一些非常好用的工具类型,能够配合我们更好的使用工具类型。

Readonly & Partial

Readonly可以将类型转换为只读对象,使用方式是Readonly<T>

一个实栗立即了解:

interface Person{
    name: string;
}
type Person2 = Readonly<Person>;

const a: Person2 = {
    name: 'wangly19'
}

const b: Person = {
    name: 'wangly19'
}

a.name = 'wangly19 new'
b.name = 'wangly19 new'
复制代码

Record

Record能够快速创建对象类型。它的使用方式是Record<K, V>,能够快速的为object创建统一的keyvalue类型。

Pick & Omit

  • Pick:主要作用是从一组属性中拿出某个属性,并将其返回,那么先来看一下实例。

Pick的使用方法是Pick<P, K>,如(P)类型中拥有name,age,desc三个属性,那么Kname则最终将取到只有name的属性,其他的将会被排出。

  • Omit:主要作用是从一组属性中排除某个属性,并将排除属性后的结果返回。

Omit的使用方法是Omit<P, K>,与Pick的结果是相反的,如果说Pick是取出,那么Omit则是过滤的效果,具体可以看下面的案例。

Exclude & Extract

  • Exclude: 从一个联合类型中排除掉属于另一个联合类型的子集

来看下,Exclude使用形式是Exclude<T, S>,如果T中的属性在S不存在那么就会返回。

interface A {
    show: boolean,
    hidden: boolean,
    status: string
}

interface B {
    show: boolean,
    name: string
}

type outPut = Exclude<keyof A, keyof B>
复制代码
  • Extract:跟Exclude相反,从从一个联合类型中取出属于另一个联合类型的子集

举一反三,如果Exclude是取差集,那么Extract就是取交集。会返回两个联合类型中相同的部分。

interface A {
    show: boolean,
    hidden: boolean,
    status: string
}

interface B {
    show: boolean,
    name: string
}

type outPut = Extract<keyof A, keyof B>
复制代码

Partial

Partial是一个将类型转为可选类型的工具,对于不明确的类型来说,需要将所有的属性转化为可选的?.形式,转换成为可选的属性类型。

TypeScript的工具类型有很多,不只是官方提供,在日常实践中,也会定义非常多的工具类型。那么在了解工具类型的同时,更多的是知晓这些工具类型是如何来的,怎么实现。

相信我,当你弄懂后,你对于使用Typescript会有一个新的认识,写起来会更加的得心应手。

看完了太多的理论东西,那么来看看在日常实践中是如何真实实践一把呢?

Dva的实践

如果使用过Dva开发的朋友可能知道,dispatch的类型提示非常的艰难,因此,在开发的时候重新定义了Dispatch的类型,用来做一些主动的类型提示。

对于Modal的类型作为一些基本定义,然后对DvaDispatch进行部分的注入和推导。

  • type拥有modalnamespaceeffects & reducers的类型推导。
  • Promise返回值的主动暴露。
  • 如何使用?
// 方案一
const dispatch: DvaDispatch<DeskTopModel> = useDispatch();

dispatch<null>({
    ...
})

// 方案二
dispatch<DeskTopModel, null>({
    ...
})
复制代码

那么DeskTopModel是什么呢?

没错,就是model的类型声明,在其中,对每一项effectsreducers都进行详细的定义,根据这些信息就可以推导出当前type的类型了。

export interface DeskTopModel {
  namespace: 'desktop',
  state: DeskTopModelState,
  effects: {
    getTableSourceData: Effect
  },
  reducers: {
    saveTableData: Reducer<DeskTopModelState>
  }
}
复制代码

对于Dva来说,很多时候都需要在Effect后做某事,这个时候有两个方式,一是callback,另外一个就是Promise回调。

而通过Promise方式,进行返回值的推导可以让使用dispatch拥有返回类型的能力。

Dva Dispatch

/**
 * ActionType, 推导当前effect & reducer
 * @default string
 */
type ActionType<M extends Model | string> = M extends Model ? 
`${M['namespace']}/${(keyof M['effects'] | keyof M['reducers']) & string}` : 
string


/**
 * dvaDispatch新增类型
 * @example
 * dispatch: DvaDispatch<Store>
 */
export type DvaDispatch<S = void> = <T = undefined, R = undefined>(action: {
  type: ActionType<
    S extends Model 
      ? S 
        : T extends Model 
          ? T 
            : string>,
  payload?: any
  loading?: boolean,
  toast?: Taro.showToast.Option
  [key: string]: any
}) => 
T extends Model 
  ? R extends undefined 
    ? undefined 
      : Promise<R> 
        : T extends undefined 
          ? undefined 
            : Promise<T>

复制代码

Service Response 实践

Service Response是什么?

在于后端通信时,会返回很多的数据,那么在使用TypeScript的时候怎么去定义这些类型呢?又怎么在团队协作中进行合作呢?

在大部分实验当中,我们是这样做的。

创建API命名空间

绝大多数数据,都是存放在API的命名空间当中。它的目录如下:

-- index.d.ts
-- api1.d.ts
-- api2.d.ts
-- api3.d.ts
...
复制代码

团队协作当中,index.d.ts多数为公共类型。而其他文件中的则是模块类型。举个例子,Request的返回类型。

declare namespace API {
  type commonResult<T = any> = {
    data: T,
    code: string,
    showMessage: false | {
      method: 'message' | 'notification',
      type: 'success' | 'error' | 'info' | 'warning',
      message: string,
      description?: string
    }
  }
}
复制代码

而对应请求方案配置则对应相应的api文件。

home.ts声明了配置转请求函数的方式。

// #home.ts
module.exports = {
  getVisualizationListApi: 'GET /service-admin/v1/visualization/table/list'
  addVisualizationItemApi: 'GET /service-admin/v1/visualization/table/add'
}
复制代码

然后对应的在types下声明一个新的.d.ts类型声明文件。比如:home.ts对应home.d.ts

那么在多人协作下,每个人负责的模块本身来说都不会冲突。在项目迭代管理中,大多数都是一个人对应一个小模块的开发节奏,彼此不会有太大的重复。

// #home.d.ts

declare namespace API {
  type VisualizationListResponse = {}
  type VisualizationActionResponse = {}
}
复制代码

所有的declare namespace API 都会合并。在namespace之间依旧可以使用API.xx(其他模块的type)来结合声明类型。

如何使用?

在进行namespace的声明定义后,可以在需要使用的地方,无需任何引入直接访问API,然后通过API.VisualizationListResponse就可以访问到定义的VisualizationListResponse类型。

如何深入学习TypeScript?

当了解TypeScript后,想学习进阶的使用方式,可以看看一些类型库的源码,这些源码内很多TypeScript的操作都能够在其中看到。

比较好的如:utility-types, 里面有一些实用的基本类型,可以对源码进行阅读,阅读难度不大,多动手实践下就会对类型有一个更加清晰的明确。

TypeScript是一把双刃剑,对开发者来说具有一定门槛,在使用不当的时候,其实对于项目来说会变得更加的复杂,可读性并没有过多的提升。

根据自身团队的实际情况,慢慢推动TypeScript的基建,保持当前生态体系下的框架和库对TypeScript的支持度良好的情况下逐步替换到TypeScript是一个不错的选择。

打个比方:如果你现在使用的是vue2,那么不妨可以考虑下,用TypeScript写组件真的好吗?

TypeScript上手需要一定的学习的学习成本和任务负担,并不是说会javaScript就会TypeScript,其中OOP的思想来说,对团队成员其实是有一定的影响的。尤其是在敏捷项目开发下,影响还是蛮大的。

因此,如果项目迭代本身高频快,那么在估量开发需求时,质量和效率很明显并不能兼得之。可以慢慢的进行推动。

TypeScript不会防止屎山的出现,也没有大多数人传言中的那么香。只是很多吹捧的人会把屎山说香。它只是一个类型系统,并没有传的那么神乎其神,能做的只是杜绝了很多奇技淫巧,让代码可以在一个较为正常的环境下进行开发。

如何推动?

  • 进行TypeScript的分享,帮助团队成员加深对TypeScript理解。
  • 使用TypeScript进行公共组件方法的书写和切换。
  • 对目前使用的框架和库进行TypeScript最佳实践。
  • TypeScript进行基础类型的定义,方便团队成员使用。
  • 关注TypeScript新动向,了解新特性。

如果文章对你有帮助,不妨动个小手,给文章点个赞吧。如果有不懂的地方可以看我资料加微信进行交流。
本文首发掘金技术社区
作者:wangly19


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK