TS应该掌握的知识

192 阅读9分钟

TS技术分享

介绍

为什么要推进TS开发?

简单来说,就一句话,因为我们的项目,不是写完就撤的项目

怎么解释?

先讲个微软的故事,在2010年微软公司的一个深夜,拥有黑眼圈的程序员们加班改bug,其中一个资深程序员抱怨“这声明的变量不写类型注释,还偷偷改数据类型,写的人也太不靠谱,这维护难搞哦”。正巧被路过的老板听见,第二天微软决定以该程序员为首,成立一个紧急小组开发一门新语言TypeScript,需求是提升大型JS项目的可靠性和可维护性。

所以,推进TS的原因是:为了我们的项目后续的维护和开发更快、更简单。

对比

js 写法,一个简单的 add 函数, 用于2数之和
function add(first, later) {
    return first + later
}

ts写法
function add(first:number, later:number):number {
    return first + later
}

上述 js写法有什么问题呢?

在我看来有两个问题,一个是js本身的问题;第二个是开发者的问题。

何解?怎么说?

1、js是弱类型语言,上述add函数本来只用于数字之和,如果入参不小心改变成了字符和**数字,**那么就别变成了拼接了,'1' + 1 => '11'。使用TS就不会这样

2、开发者的问题,这个怎么解释?

这在我看来是分为 ab 情况

a、解决 '1' + 1 => '11' 的问题,往add函数体内加入类型判断

function add(first, later) {
    if (typeof first !== 'number') {
        first = Number(first) // 不计较数值丢失
    }
    
    if (typeof later !== 'number') {
        later = Number(later) // 不计较数值丢失
    }
    return first + later
}

完成以上效果,结果代码体增加不说,如果传入的不是number,最后放回的结果还是出现bug;这种在流程中不可控的原因最难把控。

b、add函数本来是作两数之和,但是现在出现了string的情况,所以需求增加,增加string相加,这样就会导致add代码体量增加不说,还需要判断类型,心智负担会大大提升。

TS怎么解决?

type Add<T> = (f: T, l: T) => T

const add: Add<number> = (f, l) => f + l
add(1, 1) // const add: (f: number, l: number) => number

const add2: Add<string> = (f, l) => f + l
add2('1', '1') // const add: (f: string, l: string) => string

TS工作原理

这里借用社区找到的图片

ts是一门全新的语言,所以大体对它的解析还是走前端老一套解析过程:先转化成ast,再作中间层处理,最后生成js

有兴趣的可以自己去研究下

TS落地的好处

  1. 加上了类型系统,对人好,对机器,对维护都很好,增强了代码的双层可读型;对于编译器来说,类型定义可以让编译器揪出隐藏的 bug;
  2. 类型系统 + 静态分析检查 + 智能感知 提示,书写方便,使大规模的应用代码质量更高。 bug 少,维护方便,就连重构也安心;
  3. 给应用配置、应用状态、前后端接口及各种模块定义类型,整个应用由类型定义组成,多人协作更为方便、高效和安全。

TS基础

  1. ts****基础我就不多讲了,推荐以下文章:去巩固基础

juejin.cn/post/701880…

  1. ts的关键字。extend,infer,keyof,in,typeof的常规用法及关键字特性
  2. ts体操,ts也能按照js去编写从而推导类型,推荐以下文章:去了解用法

juejin.cn/post/706155…

TS项目实战(react + ts)

这里分享一些我个人的想法,可能也许会比较片面甚至错误,欢迎大家积极指正错误

函数式组件的声明方式

直接声明,定义入参类型

type AppProps = {
  message: string
  children?: React.ReactNode
}

const App = ({ message, children }: AppProps) => (
  <div>
    {message}
    {children}
  </div>
)

Hooks

useState

大部分情况下,TS 会自动为你推导 state 的类型:

// `val`会推导为boolean类型, toggle接收boolean类型参数
const [val, toggle] = React.useState(false)

// obj会自动推导为类型: {name: string}
const [obj] = React.useState({ name: 'sj' })

// arr会自动推导为类型: string[]
const [arr] = React.useState(['One', 'Two'])

使用推导类型作为接口/类型:

export default function App() {
  // user会自动推导为类型: {name: string}
  const [user] = React.useState({ name: 'sj', age: 32 })

  const showUser = React.useCallback((obj: typeof user) => {
    return `My name is ${obj.name}, My age is ${obj.age}`
  }, [])

  return <div className="App">用户: {showUser(user)}</div>
}

但是,一些状态初始值为空时(null),需要显示地声明类型:

type User = {
  name: string
  age: number
}

const [user, setUser] = React.useState<User | null>(null)

useRef

需要对指向的对象或者dom作类型约束

function MyComponent() {
  const ref1 = React.useRef<HTMLDivElement>(null)

  React.useEffect(() => {
    // ref1.current 有时候会报红,提示没有focus方法
    doSomethingWith(ref1.current.focus())
    
    
    // 所以需要先对 ref1.current 作类型约束
    const dom = ref1.current as HTMLDivElement
    doSomethingWith(dom.focus())
  })

  return <div ref={ref1}> etc </div>
}

自定义组件(antd组件自定义)

使用联合类型,对中间层做处理;

比如:你想在关闭之前有额外操作

import { Drawer, DrawerProps } from 'antd'

type InitProps = DrawerProps & {
  onCancel: (...args: any[]) => any
}

export default function Com({ onCancel, ...otherProps }: InitProps) {

  const handleClose = () => {
    doSomething() // 你在关闭之前做的事情
    onCancel && onCancel()
  }

  return <Drawer onClose={handleClose} {...otherProps}>
    xxx
  </Drawer>
}

函数 e 的类型

react有提供许多 e 的类型,MouseEvent、DragEvent、FormEvent等等,使用什么类型可根据具体场景来

import { Button } from 'antd'
import { MouseEvent, DragEvent, FormEvent } from 'react'

export default function Com({ onCancel, ...otherProps }: InitProps) {

  const handleClick = useCallback((e: MouseEvent) => {
    e.preventDefault()
  }, [])

  return <Button onClick={handleModelClick}>
    Button
  </Button>
}
  • ClipboardEvent<T = Element> 剪切板事件对象

  • DragEvent<T = Element> 拖拽事件对象

  • ChangeEvent<T = Element> Change事件对象

  • KeyboardEvent<T = Element> 键盘事件对象

  • MouseEvent<T = Element> 鼠标事件对象

  • TouchEvent<T = Element> 触摸事件对象

  • WheelEvent<T = Element> 滚轮时间对象

  • AnimationEvent<T = Element> 动画事件对象

  • TransitionEvent<T = Element> 过渡事件对象

更多的实践请看 ---> React + TypeScript实践 - 掘金

TS类型编写规范

  1. 尽量不使用 any

  2. 注释。每个类型,枚举,组件,函数,尽量加上注释;鼠标移到 showPorts 会有文档提示

    /**

    • @description 控制连接桩显示/隐藏
    • @param ports NodeListOf
    • @param show boolean
    • @example
    • 节点显示 showPorts(ports, true)
    • 节点隐藏 showPorts(ports, false) */ const showPorts = (ports: NodeListOf, show: boolean) => { for (let i = 0, len = ports.length; i < len; i = i + 1) { ports[i].style.visibility = show ? 'visible' : 'hidden' } }
  3. 类型定义存放位置;私有类型,少量使用可直接与方法或者组件放在一起;组件内大量使用在对应层级建立 model.ts 去存放;全局类型使用,在组件目录下建立(.d.ts)去编写

项目小问题

这里分享一些我个人的想法,可能也许会比较片面甚至错误,欢迎大家积极留言讨论

Q: 偏好使用 interface 还是 type 来定义类型?

A: 从用法上来说两者本质上没有区别,大家使用 React 项目做业务开发的话,主要就是用来定义 Props 以及接口数据类型。

但是从扩展的角度来说,type 比 interface 更方便拓展一些,假如有以下两个定义:

type Name = { name: string };
interface IName { name: string };

想要做类型的扩展的话,type 只需要一个&,而 interface 要多写不少代码。

type Person = Name & { age: number };
interface IPerson extends IName { age: number };

另外 type 有一些 interface 做不到的事情,比如使用|进行枚举类型的组合,使用typeof获取定义的类型等等。

不过 interface 有一个比较强大的地方就是可以重复定义添加属性,比如我们需要给window对象添加一个自定义的属性或者方法,那么我们直接基于其 Interface 新增属性就可以了。

declare global {
    interface Window { MyNamespace: any; }
}

总体来说,大家知道 TS 是类型兼容而不是类型名称匹配的,所以一般不需用面向对象的场景或者不需要修改全局类型的场合,我一般都是用 type 来定义类型。

Q: 是否允许 any 类型的出现

A: 说实话,刚开始使用 TS 的时候还是挺喜欢用 any 的,毕竟大家都是从 JS 过渡过来的,对这种影响效率的代码开发方式并不能完全接受,因此不管是出于偷懒还是找不到合适定义的情况,使用 any 的情况都比较多。

随着使用时间的增加和对 TS 学习理解的加深,逐步离不开了 TS 带来的类型定义红利,不希望代码中出现 any,所有类型都必须要一个一个找到对应的定义,甚至已经丧失了裸写 JS 的勇气。

这是一个目前没有正确答案的问题,总是要在效率和时间等等因素中找一个最适合自己的平衡。不过我还是推荐使用 TS,随着前端工程化演进和地位的提高,强类型语言一定是多人协作和代码健壮最可靠的保障之一,多用 TS,少用 any,也是前端界的一个普遍共识。

Q: 类型定义文件(.d.ts)如何放置

A: 这个好像业界也没有特别统一的规范,我的想法如下:

  • 临时的类型,直接在使用时定义

如自己写了一个组件内部的 Helper,函数的入参和出参只供内部使用也不存在复用的可能,可以直接在定义函数的时候就在后面定义。

function format(input: {k: string}[]): number[] { /***/ }
  • 组件个性化类型,直接定义在 ts(x)文件中

如 AntD 组件设计,每个单独组件的 Props、State 等专门定义了类型并 export 出去。

// Table.tsx
export type TableProps = { /***/ }
export type ColumnProps = { /***/ }
export default function Table() { /***/ }

这样使用者如果需要这些类型可以通过 import type 的方式引入来使用。

  • 范围/全局数据,定义在.d.ts 文件中

全局类型数据,这个大家毫无异议,一般根目录下有个 typings 文件夹,里面会存放一些全局类型定义。

假如我们使用了 css module,那么我们需要让 TS 识别.less 文件(或者.scss)引入后是一个对象,可以如此定义:

declare module '*.less' {
  const resource: { [key: string]: string };
  export = resource;
}

而对于一些全局的数据类型,如后端返回的通用的数据类型,我也习惯将其放在 typings 文件夹下,使用 Namespace 的方式来避免名字冲突,如此可以节省组件 import 类型定义的语句。

declare namespace EdgeApi {
  interface Department {
    description: string;
    gmt_create: string;
    gmt_modify: string;
    id: number;
    name: string;
  }
}

这样,每次使用的时候,只需要const department: EdgeApi.Department即可,节省了不少导入的精力。开发者只要能约定规范,避免命名冲突即可。

推荐文章总结

1、2021 typescript史上最强学习入门文章(2w字) - 掘金

2、接近天花板的TS类型体操,看懂你就能玩转TS了 - 掘金

3、Ts高手篇:22个示例深入讲解Ts最晦涩难懂的高级类型工具 - 掘金

4、这 30 道 TS 练习题,你能答对几道? - 掘金

5、React + TypeScript实践 - 掘金