TypeScript 类型体操,无非是语法过度嵌套而已

简介: TypeScript 类型体操,无非是语法过度嵌套而已

写这篇文章的初衷,是因为又有一个粉丝朋友被 TypeScript 的类型体操逼疯了。他跟我吐槽了一通,然后问我是不是他使用 TS 的姿势不对,为什么感觉到的全是痛苦。


当然,我自己最近也对 TypeScript 怨念颇深,因为我把自己项目中的 React 升级到了 "react": "^18.2.0" ,对应的类型 "@types/react": "^18.2.45", 也升级了。然后我的项目就像中毒了一样,报了一堆错。一些三方工具库的类型直接就不兼容了,那一瞬间就超级想要放弃 TypeScript.


所以就想趁着这个烦躁情绪还在,来跟大家好好吐槽一下 TypeScript,这个让人又爱又恨的技术方案。不然我怕过段时间我就忘记了阵痛,又开始只记得 TS 的好了。


一、初心


好多大佬都在吐槽 JavaScript 是一种弱类型的语言,这也是毛病那也是毛病。所以 TypeScript 出现了。他是 JavaScript 的超集,具有强大的类型系统。TS 的初心就在于,他想要把 JS 变成一门强类型语言。


也就是说,TS 的出现,最开始的目的就是为了限制 JS 弱类型的灵活性。可是,在发展的过程中,也不知道是 TS 本身的问题,还是某些使用者有问题,玩着玩着,大家就在绞尽脑汁想要追求类型的灵活性。

于是,类型体操诞生了。


就比如,有的人想要封装一个方法,去获取数据最后一个值的类型到底是什么。然后就一顿体操操作出来。如下

type Last<T extends any[]> = 
    T extends [...any[], infer Latest] 
    ? Latest : never

一个小小的三目运算符,叠加了好几个基础语法。


然后,我的问题就是,在强类型的逻辑里,一个数组,为什么要有不同类型的子项?


我们来梳理一下这个逻辑,假如我允许数组中存在不同类型的子项,会发生什么事情呢?我们来试试看:现在我定义一个简单的数组,子项类型可能会是 number 或者 string,于是我这样声明数组。

const arr:Array<number | string> = [1, 'string']

OK,这里还没有什么问题,然后如果我要使用这个数组呢?在使用的过程中,子项类型不同,会走向不同的逻辑,于是我们会在使用的时候对类型进行判断

const arr: Array<number | string> = [1, 'string']
arr.map(item => {
  if (typeof item === 'number') {
    return item + 1
  }
  if (typeof item === 'string') {
    return item.toLocaleUpperCase()
  }
  return item
})

从写法上,这也没有什么问题,但是我有一个问题就是,那我们使用 ts 对 arr 进行类型约束的意义在哪里?


没错,意义消失了。我们使用 TS 的初衷是为了限制 JS 类型的灵活性,但是在使用的过程中,又把 JS 类型的灵活性找回来了... 所以,回过头来思考一下我们刚才写了一个体操去获得数组最后一项的类型是什么,这个体操存在的基础就是,认可了数组子项类型的多变。


所以很多人在使用 TS 的过程中,感受不到 TS 的意义,更多的是感受到痛苦。因为我们在使用 ts 时,并没有想着去限制 JS 类型的多样性,而是在尽可能的想办法使用 ts 的语法去包容 JS 的弱类型。然后大家就在类型体操的路上越走越远。


属于典型的既要强类型,又要灵活性。不痛苦才怪。


二、类型体操,无非过度嵌套而已


TS 的类型体操,透露着一股强烈的过度嵌套味道。


就比如在 react 18 的类型声明中,对 Provider 的封装,内部是实现是这样的

interface ExoticComponent<P = {}> {
  /**
   * **NOTE**: Exotic components are not callable.
   */
  (props: P): ReactElement | null;
  readonly $$typeof: symbol;
}
interface ProviderExoticComponent<P> extends ExoticComponent<P> {
  propTypes?: WeakValidationMap<P> | undefined;
}
interface ProviderProps<T> {
  value: T;
  children?: ReactNode | undefined;
}
type Provider<T> = ProviderExoticComponent<ProviderProps<T>>;

是的,我这里要吐槽的就是 React 的类型声明


就是几个简单的属性,愣是写了好几个 interface 出来继承,泛型也是一层套一层,狗看了都要摇头。这逻辑拆分得,比函数柯里化都还要过分。关键的问题是,还能让你看得迷糊... 觉得他高深莫测... 就是主打一个有病。给我们在使用的时候带来的痛苦就是,当类型推导在某个环节断层,你又要去兼容它不报错,有的时候我都不知道咋写...


再比如我们列举一个简单的例子。我知道有的人已经开始看不懂了

type Exclude<T, K> = T extends K ? never : T
type Omit<T, K> = {
  [P in keyof T as P extends Exclude<keyof T, K> ? P : never]: T[P]
}

看上去是不是很高级。


这真的是把语法嵌套用得炉火纯青。你在 js 里面这样写,不被喷就是好的了。但是你说怪不怪,放到 TS 里,就变成高大上了????


但凡是一个正常的程序员,都知道这是基础语法的过度嵌套,上面例子虽然实现了 Omit 的功能,但是可读性那是一点都没有。不过呢,有的人会告诉你,你得学会这样搞哦,不然就是不懂 TS!

???


什么时候语法过度嵌套使用还成学习目标了?


有的地方更过分,直接整个三目运算的层层嵌套

type DeepReadonly<T> = T extends ((...args: any[]) => any) | Primitive
  ? T
  : T extends _DeepReadonlyArray<infer U>
  ? _DeepReadonlyArray<U>
  : T extends _DeepReadonlyObject<infer V>
  ? _DeepReadonlyObject<V>
  : T;

你 JS 也这样写吗?


整个所谓的 TS 类型体操,就是一个大型语法过度使用的灾难现场,这就是 TS 类型体操的本质。所以我们常常可以比较容易写出来一个体操,但是你要去读懂别人写的体操,那可就真不容易。最痛苦的是,有的时候,你还要写一个类型去兼容他的体操类型....


这,绝对不是学习和使用 TS 的正确方向。这样的思路,也无法利用 TS 给我们的工作带来任何便利和效率上的提升,反而是极大的降低了工作效率。


三、如何正确使用 TypeScript


好在我洞察了 TS 各种行业乱象,滤清了各种嘈杂的声音,回归到 TS 是一门强类型语言的本质,充分发挥这一特点,从提高开发效率的角度,找到了使用 TypeScript 的正确姿势。


这里最核心的关键,就是要理解到 TS 具备强大的类型推导能力。,做到一处声明,多处使用,其他地方全靠推导。这需要一定的架构思维来支撑我们去去构建一个完备的类型体系。


以我之前在 React 知命境中,自定义 hook 的一个案例为例,在使用层面,我的写法是这样的

const {
  loading, 
  setParam,
  list = [],
  error
} = useFetch(searchApi)

毫无 TS 痕迹。


他厉害的地方就在这里,我们会发现,虽然没有任何 TS 语法的痕迹在,但是类型已经被明确好了。包括函数的入参,返回值,所有的细节都有。


那么问题的关键就是,如何做到的呢?非常简单,利用 TS 强大的类型推导,我们只需要关注数据类型的入口即可。


首先明确入参 searchApi 的类型,中间的具体逻辑可以不看,只关注类型的部分。

export function searchApi(param?: string) {
  return new Promise<string[]>((resolve, reject) => {
    const p = (param || '').split('')
    const arr: string[] = []
    for(var i = 0; i < 10; i++) {
      const pindex = i % p.length
      arr.push(`${p[pindex] || '^ ^'} - ${Math.random()}`)
    }
    setTimeout(() => {
      if (Math.random() * 10 > 1) {
        resolve(arr)
      } else {
        reject('请求异常,请重新尝试!')
      }
    }, 600)
  })
}

然后再封装自定义 hook 时,利用 ts 的类型兼容性和类型推导的特性,把这里的细节逻辑封装在 useFetch 中

type API<T, P> = (param?: P) => Promise<T>
export default function useFetch<T, P>(api: API<T, P>) {
  const param = useRef<P>()
  const [list, setList] = useState<T>()
  const [error, setError] = useState('')
  const [loading, setLoading] = useState(true)
  function getList() {
    api(param.current).then(res => {
      setList(res)
      setLoading(false)
      setError('')
    }).catch(err => {
      setLoading(false)
      setError(err)
    })
  }
  useLayoutEffect(() => {
    loading && getList()
  }, [loading])
  return { 
    param, 
    setParam: useCallback((p: P) => param.current = p, []),
    list, 
    error, 
    loading, 
    setLoading 
  }
}

在实践中,我们会大量运用类型推导来简化 TS 的编写,只需要在类型入口和出口去明确类型的声明。以及偶尔在类型推导脱节的时候去重新明确类型声明。


在列举一个例子,很多年前我在 github 上基于 react hooks 封装了一个小型的状态管理工具 moz,我也做到了使用时无 TS 痕迹,能够自动推导出定义在 store 中的具体数据类型


地址, https://github.com/yangbo5207/moz,欢迎大家 star 与使用,这是一个非常灵活,超级轻量,使用简单,非常适合小范围状态共享的状态管理库。


四、如何学习 TypeScript


我们只需要明白一个道理,就能具备学好 TS 的基础,那就是:类型体操是基础语法的嵌套。因此,我们只需要去学习 TS 的基础语法就好了。这样结合官方文档,我们就能学好 TS。


除此之外,在我的小册《JavaScript 核心进阶》中,我专门把 TS 学习最重要最核心的部分抽离出来分为几个部分,明确了一个通熟易懂的学习思路,给大家提供了一个非常有用的学习指引


  • 学习 TypeScript 的必要性
  • 观察 TypeScript 的实践运用
  • 泛型
  • 类型推导是核心
  • 常用高级类型
  • 准确理解类型兼容
相关文章
|
9天前
|
JavaScript 编译器
TypeScript中类型守卫:缩小类型范围的艺术
【4月更文挑战第23天】TypeScript中的类型守卫是缩小类型范围的关键技术,它帮助我们在运行时确保值的精确类型,提升代码健壮性和可读性。类型守卫包括`typeof`(检查原始类型)、`instanceof`(检查类实例)和自定义类型守卫。通过这些方法,我们可以更好地处理联合类型、泛型和不同数据源,降低运行时错误,提高代码质量。
|
2天前
|
JavaScript 安全 前端开发
【TypeScript技术专栏】TypeScript中的类型推断与类型守卫
【4月更文挑战第30天】TypeScript的类型推断与类型守卫是提升代码安全的关键。类型推断自动识别变量类型,减少错误,包括基础、上下文、最佳通用和控制流类型推断。类型守卫则通过`typeof`、`instanceof`及自定义函数在运行时确认变量类型,确保类型安全。两者结合使用,优化开发体验,助力构建健壮应用。
|
2天前
|
JavaScript 前端开发 开发者
【TypeScript技术专栏】TypeScript类型系统与接口详解
【4月更文挑战第30天】TypeScript扩展JavaScript,引入静态类型检查以减少错误。其类型系统包括基本类型、数组等,而接口是定义对象结构的机制。接口描述对象外形,不涉及实现,可用于规定对象属性和方法。通过声明、实现接口,以及利用可选、只读属性,接口继承和合并,TypeScript增强了代码的健壮性和维护性。学习和掌握TypeScript的接口对于大型项目开发至关重要。
|
3天前
|
JavaScript 前端开发
TypeScript基础类型
TypeScript基础类型
|
3天前
|
JavaScript 前端开发
typescript 混合类型
typescript 混合类型
|
8天前
|
JavaScript 前端开发 开发者
类型检查:结合TypeScript和Vue进行开发
【4月更文挑战第24天】TypeScript是JavaScript超集,提供类型注解等特性,提升代码质量和可维护性。Vue.js是一款高效前端框架,两者结合优化开发体验。本文指导如何配置和使用TypeScript与Vue:安装TypeScript和Vue CLI,创建Vue项目时选择TypeScript支持,配置`tsconfig.json`,编写`.tsx`组件,最后运行和构建项目。这种结合有助于错误检查和提升开发效率。
|
9天前
|
JavaScript 编译器 开发者
TypeScript中的类型推断机制:原理与实践
【4月更文挑战第23天】TypeScript的类型推断简化编码,提高代码可读性。编译器基于变量初始值或上下文推断类型,若新值不兼容则报错。文章深入探讨了类型推断原理和实践,包括基本类型、数组、函数参数与返回值、对象类型的推断,并提醒注意类型推断的限制,如非万能、类型兼容性和适度显式指定类型。了解这些能帮助更好地使用TypeScript。
|
9天前
|
JavaScript 前端开发 编译器
TypeScript中的高级类型:联合类型、交叉类型与条件类型深入解析
【4月更文挑战第23天】探索TypeScript的高级类型。这些特性增强类型系统的灵活性,提升代码质量和维护性。
|
9天前
|
JavaScript 安全 编译器
TypeScript中类型断言的使用与风险
【4月更文挑战第23天】TypeScript中的类型断言用于显式指定值的类型,但在不恰当使用时可能引发运行时错误或降低代码可读性。
|
2月前
|
JavaScript 安全
TypeScript 中的高级类型转换技术:映射类型、条件类型和类型推断
TypeScript 中的高级类型转换技术:映射类型、条件类型和类型推断