前言
过年期间在家里没事,把zustand的源码看了一遍,看完后我最大的收获就是ts水平突飞猛进,再去刷那些类型体操题目就变得简单了,下面和大家分享一下zustand库是怎么定义ts类型的。
ts类型推断
个人认为ts最大的作用有两个,一个是类型约束,另外一个是类型推断。
- 类型约束也叫类型安全,在编译阶段就能发现语法错误,可以有效减少低级错误。
- 类型推断,当你没有标明变量的类型时,编译器会根据一些简单的规则来推断你定义的变量的类型
这一篇主要和大家分享类型推断,类型推断主要有以下几种情况。
根据变量的值自动推导类型
函数返回值自动推断
函数中如果有条件分支,推导出来的返回值类型是所有分支的返回值类型的联合类型
ts的类型推导方式是懒推导,也就是说不会实际执行代码。
上图中如果实际执行了,c的类型是能确认为null的。
使用范型推导
可以看到按照上面写法,对象合并推导不出来,如果能推导出来u3应该等于 {name: string, age: number}
。
这时候我们可以借助范型来推导
可以给上面代码简写为这样,编辑器也能推导出来
实战
实现pick方法
从一个对象中,返回指定的属性名称。
上面代码中定义了两个范型T和U,T表示对象,U被限定为T的属性名(U extends keyof T
),返回值的类型为{[K in U]: T[K]}
,in的作用就是遍历U这个数组。
可以看到数组元素被限制了只能是user对象里的key
也正确的推导出来了
实现useRequest
先看一个例子
import { useEffect, useState } from 'react';
// 模拟请求接口,返回用户列表
function getUsers(): Promise<{ name: string }[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
name: 'tom',
},
{
name: 'jack',
},
]);
}, 1000);
})
}
const App = () => {
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<Awaited<ReturnType<typeof getUsers>>>([]);
const [error, setError] = useState(false);
useEffect(() => {
setLoading(true);
getUsers().then((res) => {
setUsers(res);
}).catch(() => {
setError(true);
}).finally(() => {
setLoading(false);
})
}, []);
if (loading) {
return (
<div>loading...</div>
)
}
if (error) {
return (
<div>error</div>
)
}
return (
<div
>
{users.map(u => (
<div key={u.name}>{u.name}</div>
))}
</div>
);
};
export default App;
上面这个例子实现了从后端请求用户列表,然后渲染出来。为了提高用户体验,在加载数据时,加了一个loading,当请求出错时,告诉用户请求失败。
代码比较简单我就不一一讲解了,有行代码需要注意一下。
const [users, setUsers] = useState<Awaited<ReturnType<typeof getUsers>>>([]);
typeof getUsers
获取getUsers函数类型ReturnType
获取某个函数的返回值Awaited
如果函数返回值为Promise,这个可以获取到最终的值类型。
可以看到,正确的获取到了getUsers函数的返回值类型。
然而一个很简单的功能需要写那么多代码,肯定是不合理的,那么我们给简化一下。目前市面上已经有不少库来解决这个问题了,比如react-query或ahooks库里的useRequest,都可以解决这个问题,我这里分享的不是具体代码实现,而是怎么写ts。
封装useRequest
import { useEffect, useState } from 'react';
export function useRequest<T extends () => Promise<unknown>>(
fn: T,
): {
loading: boolean;
error: boolean;
data: Awaited<ReturnType<T>>;
} {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [data, setData] = useState<any>();
useEffect(() => {
setLoading(true);
fn().then(res => {
setData(res);
}).catch(() => {
setError(true);
}).finally(() => {
setLoading(false);
});
}, [fn])
return {
loading,
error,
data,
};
}
改造app.tsx文件,使用useRequest
import { useRequest } from './useRequest';
// 模拟请求接口,返回用户列表
function getUsers(): Promise<{ name: string }[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
name: 'tom',
},
{
name: 'jack',
},
]);
}, 1000);
})
}
const App = () => {
const { loading, data: users, error } = useRequest(getUsers);
if (loading) {
return (
<div>loading...</div>
)
}
if (error) {
return (
<div>error</div>
)
}
return (
<div
>
{users.map(u => (
<div
key={u.name}
>
{u.name}
</div>
))}
</div>
);
};
export default App;
对比最开始的代码,是不是简单了很多。
useRequest.tsx
代码也很简单,首先使用了范型限制fn只能是一个函数,返回值还必须是Promise
。这个hooks返回值loading和error就不说了,主要是data,这个data要求和传进来的方法返回值一致,前面说过,可以使用Awaited<ReturnType<T>>
获取函数的返回类型。
但是上面代码可能会导致bug,看下面代码,如果请求失败,users应该是空的,直接这样使用就会报错了。改造一下,当error为false的时候data为正常类型,error为true的时候data为null,这里可以使用联合类型。
加了一个判断后,下面就不会报错了。ts在某些时候,真的可以避免一些低级错误,我相信如果没有这个限制,肯定有人在写代码的时候不加判断直接用users。
如果请求接口的函数需要参数怎么办,下面来实现一下。
使用Parameters获取传入函数的参数类型
多个参数也是支持的
zustand
zustand是一个react状态管理库,使用起来比较简单没啥心智负担,所以我一直在用。
上面带着大家入门了ts的类型推断,下面给大家分享一下zustand的ts定义。我看完zustand源码后,发现这个库的ts定义比功能实现还复杂,这里我只给大家分享ts,具体实现掘金已经有很多大佬写过了,我就不分享了。
先从一个最简单的例子开始
import { create } from 'zustand';
interface State {
count: number;
}
interface Action {
inc: () => void;
}
export const useStore = create<State & Action>((set) => ({
count: 1,
inc: () => set((state) => ({count: state.count + 1})),
}));
create
方法的定义
type Create = {
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): UseBoundStore<Mutate<StoreApi<T>, Mos>>
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer