TS深入浅出系列 - 面向类型编程

1,037 阅读5分钟

解决重复,成就自我

小李是练习时长两年半的前端练习生,最近刚入职,产品让他去做做修改标题之类的小事,需要将产品未定义标题的地方统一标题为 xxx,小李审查了下代码,以前的标题是用xxx组件库实现的,传递标题时展示,不传递则不展示标题,统一标题是这个产品线的需求,不应该侵入到组件库,而且人微言轻,哪儿敢和组件开发团队提需求,所以只好自己包装组件解决。

// A 组件
type Props = {
  title: string
}

const A = ({ title }: Props) => <h1>{title}</h1>
// WrapA组件
type WrapProps = {
  title?: string
}
const WrapA = ({ title = 'xxx' }: WrapProps) => <A title={title}/>

存在的问题

  1. 与原有类型断开连接
  2. 复用困难

假如定制标题传递默认属性更多:

// A 组件
type Props = {
  title: string
  color: string
  ...
}

const A = (props: Props) => <h1 {...props}>{props.title}</h1>
// WrapA组件
type WrapProps = {
  title?: string
  color?: string
  ...
}
const defaultProps: WrapProps = {
  title = 'xxx',
  color = 'yyy',
  ...
}
const WrapA = (props: WrapProps) => <A {...props} />
WrapA.defaultProps = defaultProps

类比到js里面

// 实现
const a = {
  title: '123'
}
// 转化为
// 手写
const b = {
  _title: '123'
}
let _b = {}
// 半自动
for (const key in Object.keys(a)) {
  _b[`_${key}`] = a[key]
}
// 输入 {
//  title: '123'}
// 返回 {
//  _title: '123'
// }
function warpObj(T) {
  let _b = {}
  for (const K in Object.keys(T)) {
    _b[`_${K}`] = T[K]
  }
  return _b
}
const a = {
  title: 'xx',
  ...
}
const b = warpObj(a)

分析:

  1. 遍历
  2. 修改
  3. 封装

ts里面的类型编程也可以类比到熟悉的js编程里面

基础知识

为了学习面向类型编程,我们首先要学习一些基础知识

keyof 类型运算符

keyof 类型运算符 接受对象类型,并生成其键的字符串或数字文字并集:

type Props = {
  title: string
  xxx: boolean
  aaa: number
}
type PropsKey = keyof Props
//          ^ = type PropsKey = 'title' | 'xxx' | 'aaa'

索引访问类型

使用索引访问类型来查找另一种类型的特定属性:

type Props = {
  title: string
  xxx: boolean
  aaa: number
}

type TitleType = Props['title']
//           ^ = type TitleType = string

类型映射

类型映射:一种类型 -> 另一种类型

// copy
type Props = {
  title: string
  xxx: boolean
  aaa: number
}
type PropsKey = keyof Props
//          ^ = type PropsKey = 'title' | 'xxx' | 'aaa'

type CopyProps = {
  [K in PropsKey]: Props[K]
}
//           ^ = type CopyProps = {
//   title: string
//   xxx: boolean
//   aaa: number
// }

映射修饰符

在映射期间可以应用两个附加的修饰符:readonly?。分别影响可变性和选择性。可以通过添加-+前缀来删除或添加这些修饰符。如果不添加前缀,则假定为+

type Partial<T> = {
  [K in keyof T]?: T[K]
}
type Props = {
  title: string
}
type PartialProps = Partial<Props>
//              ^ = type PartialProps = {
//   title?: string
// }

对比JS

// T 泛型
// K 类型参数
// T[K] 索引访问类型
// ? 映射修饰符
// [K in keyof T] 映射
type Partial<T> = {
  [K in keyof T]?: T[K]
}
// T 参数
// K 内部变量
// T[K] 索引访问
// _b[`_${K}`] 索引访问
// for in loop
function Partial(T) {
  let _b = {}
  for (const K in Object.keys(T)) {
    _b[`_${K}`] = T[K]
  }
  return _b
}

扩展实现

  1. 必填
// -?
type Required<T> = {
  [K in keyof T]-?: T[K]
}
  1. 只读
// +readonly
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}
  1. 可修改
// -readonly
type Readonly<T> = {
  -readonly [K in keyof T]: T[K]
}

更进一步

条件类型

类似于JS三元表达式

const a = 1
const b = 2
const c = a > b ? a : b
//    ^ = const c = b
type P = 1
type R = P extends number ? true : false
//   ^ = R type R = true

never

never 短路效应: never与其他类型作为联合类型时将被忽略

type P = never | string
//   ^ = type P = string

利用 never 过滤 null | undefined

type P = {
  title?: string
}
type NonNullable<T> = T extends (null | undefined) ? never : T
type PTitle = P['title']
//        ^ = type PTitle = undefined | string
type Title = NonNullable<PTitle>
//       ^ = type Title = string

模版字符串类型

模版字符串类型建立在字符串文字类型的基础上,并具有通过联合扩展为许多字符串的能力。 它们的语法与ES6中的模版字符串相同,但用于类型。当与具体字符串类型一起使用时,模版字符串通过连接内容来产生新的字符串类型。

  1. 基础使用
type World = 'world'

type Greeting = `hello ${World}`
//          ^ = type Greeting = "hello world"
  1. 与联合类型一起使用
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
//              ^ = type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

类型重映射

使用映射类型中的as子句重新映射映射类型中的键:

type IndexObject = {[index: string]: any}

type Underline<T extends IndexObject> = {
  [K in keyof T as `_${string & K}`]: T[K]
}
type P = {
  title: string
}
type UnderlineP = Underline<P>
//            ^ = type UnderlineP = {
//  _title: string
// }

为举例的JS代码写上类型

type IndexObject = {[index: string]: any}

type Underline<T extends IndexObject> = {
  [K in keyof T as `_${string & K}`]: T[K]
}

function warpObj<T extends IndexObject>(t: T): Underline<T> {
  let _b = {} as Underline<T>
  for (const k in Object.keys(t)) {
    _b[`_${k}` as keyof Underline<T>] = t[k]
  }
  return _b
}
warpObj({ title: 1 })._title

infer

使用infer关键字从真实分支中进行比较的类型进行推断,

  1. 推断数组元素类型
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type
const a = [
  1,
  '2',
  false
]
type ArrElement = Flatten<typeof a>
//            ^ = type ArrElement = string | number | boolean
  1. 推断函数返回值
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

type Num = GetReturnType<() => number>;
//     ^ = type Num = number

  1. 推断函数参数
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

type Num = Parameters<() => number>;
//     ^ = type Num = number

组合使用

实现useRequest:

  1. 函数必须返回Promise
  2. 对函数返回Promise进行拆包
  3. 函数参数必填提示(函数内部参数为必填时 必须传递)函数重载
import React from 'react'

// 辅助类型
type RequstPartialFC<P> = (params?: P) => Promise<any>
type RequstRequiredFC<P> = (params: P) => Promise<any>
// Promise 拆包
type PromiseType<T> = T extends Promise<infer D> ? D : never

type OtherConfig<T> = {
  initFetch?: boolean
  initState?: T
  onError?: (error: any) => void
}

// 辅助函数
// 仅当key变化时执行
export function useEffectKey<T>(effect: Function, key: T) {
  const ref = React.useRef<T>(key)
  React.useEffect(() => {
    // ref.current !== key时执行
    if (ref.current !== key) {
      effect()
    }
    ref.current = key
  }, [effect, key])
}
// 函数重载
function useRequest<P extends any = any,T extends RequstRequiredFC<P> = RequstRequiredFC<P>, D extends PromiseType<ReturnType<T>> = PromiseType<ReturnType<T>>>(
  request: T,
  params: P,
  config?: OtherConfig<D>
): [
  D | undefined,
  {
    retry: (params?: P) => Promise<void>
    loading: boolean
    error: boolean
  }
]
// 函数重载
function useRequest<P extends any,T extends RequstPartialFC<P>, D extends PromiseType<ReturnType<T>>>(
  request: T,
  params?: P,
  config?: OtherConfig<D>
): [
  D | undefined,
  {
    retry: (params?: P) => Promise<void>
    loading: boolean
    error: boolean
  }
]
// 函数实现
function useRequest<P extends any, T extends RequstRequiredFC<P>  | RequstPartialFC<P>, D extends PromiseType<ReturnType<T>>>(
  request: T,
  params: P,
  { initFetch = true, initState, onError = () => {} }: OtherConfig<D> = {}
): [
  D | undefined,
  {
    retry: (params?: P) => Promise<void>
    loading: boolean
    error: boolean
  }
] {
  const [data, setData] = React.useState(initState)
  const [loading, setLoading] = React.useState(!!initFetch)
  const [error, setError] = React.useState(false)
  const fetchNum = React.useRef<number>(0)
  const [key, setKey] = React.useState(0)
  const getData = React.useCallback(
    async (localParmas?: P) => {
      if (loading && !(initFetch && fetchNum.current === 0)) {
        return
      }
      let num = fetchNum.current
      localParmas = localParmas || params
      fetchNum.current++
      setLoading(true)
      try {
        const res = await request(localParmas)
        // 过期请求
        if (fetchNum.current !== num) {
          setError(false)
        } else {
          setData(res)
          setError(false)
        }
      } catch (error) {
        console.error('[useRequest]', error)
        setError(true)
        onError(error)
      } finally {
        setLoading(false)
      }
    },
    [loading, initFetch, params, onError, request]
  )
  React.useEffect(() => {
    if (initFetch) {
      setKey(pre => pre + 1)
    }
  }, [initFetch])
  // key 变化时请求
  useEffectKey(getData, key)
  return [data, {
    retry: getData,
    loading,
    error
  }]
}

async function getUser() {
  return new Promise<{
    name: string
    age: number
  }>((relove) => relove({
    name: 'xxx',
    age: 24
  }))
}

async function getDetail(id: number) {
  return new Promise<{
    content: string
    id: number
  }>((relove) => relove({
    content: 'xxx',
    id,
  }))
}

const [data] = useRequest(getUser)
const [detail] = useRequest(getDetail, 1)

探索发现

  1. 探索使用 Typescript 内置辅助类型PickRecord...

  2. 探索第三方包类型实现,学习或改进(ReduxRematch

  3. 使用面向类型编程技巧改造项目中的any