解决重复,成就自我
小李是练习时长两年半的前端练习生,最近刚入职,产品让他去做做修改标题之类的小事,需要将产品未定义标题的地方统一标题为 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}/>
存在的问题
- 与原有类型断开连接
- 复用困难
假如定制标题传递默认属性更多:
// 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)
分析:
- 遍历
- 修改
- 封装
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
}
扩展实现
- 必填
// -?
type Required<T> = {
[K in keyof T]-?: T[K]
}
- 只读
// +readonly
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
- 可修改
// -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中的模版字符串相同,但用于类型。当与具体字符串类型一起使用时,模版字符串通过连接内容来产生新的字符串类型。
- 基础使用
type World = 'world'
type Greeting = `hello ${World}`
// ^ = type Greeting = "hello world"
- 与联合类型一起使用
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关键字从真实分支中进行比较的类型进行推断,
- 推断数组元素类型
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
- 推断函数返回值
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type Num = GetReturnType<() => number>;
// ^ = type Num = number
- 推断函数参数
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type Num = Parameters<() => number>;
// ^ = type Num = number
组合使用
实现useRequest:
- 函数必须返回
Promise - 对函数返回
Promise进行拆包 - 函数参数必填提示(函数内部参数为必填时 必须传递)函数重载
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)
探索发现
-
探索使用 Typescript 内置辅助类型
Pick、Record... -
探索第三方包类型实现,学习或改进(
Redux、Rematch) -
使用面向类型编程技巧改造项目中的
any