本文根据日常开发实践,参考优秀文章、文档,来说说 TypeScript
是如何较优雅的融入 React
项目的。
温馨提示:日常开发中已全面拥抱函数式组件和 React Hooks
,class
类组件的写法这里不提及。
前沿
import React, { useState } from 'react';
// 以后将被替代成
import { useState } from 'react';
import * as React from 'react';
复制代码
基础介绍
基本类型
type BasicTypes = {
message: string;
count: number;
disabled: boolean;
names: string[]; // or Array<string>
id: string | number; // 联合类型
}
复制代码
联合类型
一般的联合类型,没什么好说的,这里提一下非常有用,但新手经常遗忘的写法 —— 字符字面量联合。
type UnionsTypes = {
method: string; // ❌ bad,可以传入任意字符串
};
复制代码
type UnionsTypes = {
method: 'get' | 'post'; // ✅ good 只允许 'get'、'post' 字面量
};
复制代码
对象类型
type ObjectTypes = {
obj3: {
id: string;
title: string;
};
objArr: {
id: string;
title: string;
}[]; // 对象数组,or Array<{ id: string, title: string }>
};
复制代码
type ObjectTypes = {
obj: object; // ❌ bad,不推荐
obj2: {}; // ❌ bad 几乎类似 object
};
复制代码
type ObjectTypes = {
objBetter: Record<string, unknown>; // ✅ better,代替 obj: object
// 对于 obj2: {}; 有三种情况:
obj2Better1: Record<string, unknown>; // ✅ better 同上
obj2Better2: unknown; // ✅ any value
obj2Better3: Record<string, never>; // ✅ 空对象
/** Record 更多用法 */
dict1: {
[key: string]: MyTypeHere;
};
dict2: Record<string, MyTypeHere>; // 等价于 dict1
};
复制代码
// 意思就是,泛型 K 的集合作为返回对象的属性,且值类型为 T
type Record<K extends keyof any, T> = {
[P in K]: T;
};
复制代码
interface PageInfo {
title: string;
}
type Page = 'home' | 'about' | 'contact';
const nav: Record<Page, PageInfo> = {
about: { title: 'about' },
contact: { title: 'contact' },
// TS2322: Type '{ about: { title: string; }; contact: { title: string; }; hoem: { title: string; }; }'
// is not assignable to type 'Record<Page, PageInfo>'. ...
hoem: { title: 'home' },
};
nav.about;
复制代码
好处:
当你书写 home
值时,键入 h
常用的编辑器有智能补全提示;
home
拼写错误成 hoem
,会有错误提示,往往这类错误很隐蔽;
收窄接收的边界。
函数类型
type FunctionTypes = {
onSomething: Function; // ❌ bad,不推荐。任何可调用的函数
onClick: () => void; // ✅ better ,明确无参数无返回值的函数
onChange: (id: number) => void; // ✅ better ,明确参数无返回值的函数
onClick(event: React.MouseEvent<HTMLButtonElement>): void; // ✅ better
};
复制代码
可选属性
type OptionalTypes = {
optional?: OptionalType; // 可选属性
};
复制代码
type AppProps = {
name: string;
age?: number;
[propName: string]: any;
};
const YourComponent = ({ name, age, ...other }: AppProps) => (
<div>
{`Hello, my name is ${name}, ${age || 'unknown'}`} <Other {...other} />
</div>
);
复制代码
React Prop 类型
export declare interface AppProps {
children1: JSX.Element; // ❌ bad, 没有考虑数组类型
children2: JSX.Element | JSX.Element[]; // ❌ 没考虑字符类型
children3: React.ReactChildren; // ❌ 名字唬人,工具类型,慎用
children4: React.ReactChild[]; // better, 但没考虑 null
children: React.ReactNode; // ✅ best, 最佳接收所有 children 类型
functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点
style?: React.CSSProperties; // React style
onChange?: React.FormEventHandler<HTMLInputElement>; // 表单事件! 泛型参数即 `event.target` 的类型
}
复制代码
更多参考资料
函数式组件
熟悉了基础的 TypeScript
使用 与 React
内置的一些类型后,我们该开始着手编写组件了。
type AppProps = { message: string }; /* 也可用 interface */
const App = ({ message }: AppProps) => <div>{message}</div>; // 无大括号的箭头函数,利用 TS 推断。
复制代码
type AppProps = { title: string };
const App: React.FC<AppProps> = ({ children, title }) => <div title={title}>{children}</div>;
复制代码
React.FC
(or FunctionComponent
)是显式返回的类型,而"普通函数"版本则是隐式的(有时还需要额外的声明)。
React.FC
对于静态属性如 displayName
,propTypes
,defaultProps
提供了自动补充和类型检查。
React.FC
提供了默认的 children
属性的大而全的定义声明,可能并不是你需要的确定的小范围类型。
2 和 3 都会导致一些问题。有人不推荐使用。
目前 React.FC
在项目中使用较多。因为可以偷懒,还没碰到极端情况。
Hooks
项目基本上都是使用函数式组件和 React Hooks
。接下来介绍常用的用 TS 编写 Hooks 的方法。
useState
import { useState } from 'react';
// ...
const [val, toggle] = useState(false);
// val 被推断为 boolean 类型
// toggle 只能处理 boolean 类型
复制代码
type AppProps = { message: string };
const App = () => {
const [data] = useState<AppProps | null>(null);
// const [data] = useState<AppProps | undefined>();
return <div>{data && data.message}</div>;
};
复制代码
// data && data.message
data?.message
复制代码
useEffect
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs]
);
// ❌ bad example! setTimeout 会返回一个记录定时器的 number 类型
// 因为简写,箭头函数的主体没有用大括号括起来。
return null;
}
复制代码
// 1. 是一个函数
// 2. 无参数
// 3. 无返回值 或 返回一个清理函数,该函数类型无参数、无返回值 。
type EffectCallback = () => (void | (() => void | undefined));
复制代码
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(() => {
const timer = setTimeout(() => {
/* do stuff */
}, timerMs);
// 可选
return () => clearTimeout(timer);
}, [timerMs]);
// ✅ 确保函数返回 void 或一个返回 void|undefined 的清理函数
return null;
}
复制代码
// ❌ bad
useEffect(async () => {
const { data } = await ajax(params);
// todo
}, [params]);
复制代码
// ✅ better
useEffect(() => {
(async () => {
const { data } = await ajax(params);
// todo
})();
}, [params]);
// 或者 then 也是可以的
useEffect(() => {
ajax(params).then(({ data }) => {
// todo
});
}, [params]);
复制代码
useRef
useRef
一般用于两种场景
引用 DOM
元素;
不想作为其他 hooks
的依赖项,因为 ref
的值引用是不会变的,变的只是 ref.current
。
const ref1 = useRef<HTMLElement>(null!);
const ref2 = useRef<HTMLElement | null>(null);
复制代码
function MyComponent() {
const ref1 = useRef<HTMLElement>(null!);
useEffect(() => {
doSomethingWith(ref1.current);
// 跳过 TS null 检查。e.g. ref1 && ref1.current
});
return <div ref={ref1}> etc </div>;
}
复制代码
function TextInputWithFocusButton() {
// 初始化为 null, 但告知 TS 是希望 HTMLInputElement 类型
// inputEl 只能用于 input elements
const inputEl = React.useRef<HTMLInputElement>(null);
const onButtonClick = () => {
// TS 会检查 inputEl 类型,初始化 null 是没有 current 上是没有 focus 属性的
// 你需要自定义判断!
if (inputEl && inputEl.current) {
inputEl.current.focus();
}
// ✅ best
inputEl.current?.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
复制代码
useReducer
使用 useReducer
时,多多利用 Discriminated Unions 来精确辨识、收窄确定的 type
的 payload
类型。一般也需要定义 reducer
的返回类型,不然 TS 会自动推导。
const initialState = { count: 0 };
// ❌ bad,可能传入未定义的 type 类型,或码错单词,而且还需要针对不同的 type 来兼容 payload
// type ACTIONTYPE = { type: string; payload?: number | string };
// ✅ good
type ACTIONTYPE =
| { type: 'increment'; payload: number }
| { type: 'decrement'; payload: string }
| { type: 'initial' };
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - Number(action.payload) };
case 'initial':
return { count: initialState.count };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count} <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
</>
);
}
复制代码
useContext
一般 useContext
和 useReducer
结合使用,来管理全局的数据流。
interface AppContextInterface {
state: typeof initialState;
dispatch: React.Dispatch<ACTIONTYPE>;
}
const AppCtx = React.createContext<AppContextInterface>({
state: initialState,
dispatch: (action) => action,
});
const App = (): React.ReactNode => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppCtx.Provider value={{ state, dispatch }}>
<Counter />
</AppCtx.Provider>
);
};
// 消费 context
function Counter() {
const { state, dispatch } = React.useContext(AppCtx);
return (
<>
Count: {state.count} <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
</>
);
}
复制代码
自定义 Hooks
Hooks
的美妙之处不只有减小代码行的功效,重点在于能够做到逻辑与 UI 分离。做纯粹的逻辑层复用。
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // 推断出 [boolean, typeof load],而不是联合类型 (boolean | typeof load)[]
}
复制代码
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as [
boolean,
(aPromise: Promise<any>) => Promise<any>
];
}
复制代码
function tuplify<T extends any[]>(...elements: T) {
return elements;
}
function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // type is (number | (() => void))[]
}
function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {
}).current;
return tuplify(numberValue, functionValue); // type is [number, () => void]
}
复制代码
扩展
工具类型
学习 TS 好的途径是查看优秀的文档和直接看 TS 或类库内置的类型。这里简单做些介绍。
// foo 函数原作者并没有考虑会有人需要返回值类型的需求,利用了 TS 的隐式推断。
// 没有显式声明返回值类型,并 export,外部无法复用
function foo(bar: string) {
return { baz: 1 };
}
// TS 提供了 ReturnType 工具类型,可以把推断的类型吐出
type FooReturn = ReturnType<typeof foo>; // { baz: number }
复制代码
function foo() {
return {
a: 1,
b: 2,
subInstArr: [
{
c: 3,
d: 4,
},
],
};
}
type InstType = ReturnType<typeof foo>;
type SubInstArr = InstType['subInstArr'];
type SubIsntType = SubInstArr[0];
const baz: SubIsntType = {
c: 5,
d: 6, // type checks ok!
};
// 也可一步到位
type SubIsntType2 = ReturnType<typeof foo>['subInstArr'][0];
const baz2: SubIsntType2 = {
c: 5,
d: 6, // type checks ok!
};
复制代码
同理工具类型 Parameters
也能推断出函数参数的类型。
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
复制代码
T extends (...args: any) => infer R ? R : any;
的意思是 T 能够赋值给 (...args: any) => any
的话,就返回该函数推断出的返回值类型 R
。
defaultProps
默认值问题。
type GreetProps = { age: number } & typeof defaultProps;
const defaultProps = {
age: 21,
};
const Greet = (props: GreetProps) => {
// etc
};
Greet.defaultProps = defaultProps;
复制代码
type GreetProps = { age?: number };
const Greet = ({ age = 21 }: GreetProps) => {
// etc
};
复制代码
消除魔术数字/字符
本人比较痛恨的一些代码点。
if (status === 0) {
// ...
} else {
// ...
}
// ...
if (status === 1) {
// ...
}
复制代码
// enum.ts
export enum StatusEnum {
Doing, // 进行中
Success, // 成功
Fail, // 失败
}
//index.tsx
if (status === StatusEnum.Doing) {
// ...
} else {
// ...
}
// ...
if (status === StatusEnum.Success) {
// ...
}
复制代码
// 对象常量
export const StatusEnum = {
Doing: 0, // 进行中
Success: 1, // 成功
Fail: 2, // 失败
};
复制代码
export declare type Position = 'left' | 'right' | 'top' | 'bottom';
let position: Position;
// ...
// TS2367: This condition will always return 'false' since the types 'Position' and '"lfet"' have no overlap.
if (position === 'lfet') { // 单词拼写错误,往往这类错误比较难发现
// ...
}
复制代码
延伸:策略模式消除 if、else
if (status === StatusEnum.Doing) {
return '进行中';
} else if (status === StatusEnum.Success) {
return '成功';
} else {
return '失败';
}
复制代码
// 对象常量
export const StatusEnumText = {
[StatusEnum.Doing]: '进行中',
[StatusEnum.Success]: '成功',
[StatusEnum.Fail]: '失败',
};
// ...
return StatusEnumText[status];
复制代码
评论