组件状态和属性声明
函数组件
函数组件通过 React.FC<Props> 和 useState<State> 定义属性和状态
import { FC } from 'react';
interface TestProps {
val: string;
onClick?: () => void;
}
const Test: FC<TestProps> = ({ val, onClick, children }) => {
const item = useState<string>('');
const itemRef = useRef<string>(null);
const handledList = useMemo<number[]>(
() => list.map((i, idx) => idx),
[list]
);
return <div></div>;
};
export default Test;
类组件
类组件通过 React.Component<Props, State> | React.PureComponent<Props, State> 声明组件的属性和状态
import { Component } from 'react';
interface State {
count: number;
}
interface Props {
list: string[];
}
export default class ClassComponent extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { count: 0 };
}
}
// 使用
<ClassComponet list={['1']}></ClassComponet>;
声明静态资源
TypeScript 认为导入的头文件必须是一个模块,如果它没法识别出导入的文件路径是一个模块,它会报错,所以它不能像 js 那种直接把样式表或者图片资源这种静态资源导入,会报错,解决方案是声明这种路径是一个模块,涉及到覆盖范围一般是在项目根目录下声明就好,比较好的实践是 src/types.d.ts 或者 src/typings.d.ts,标明是 d.ts 文件,有个好处就是会触发 eslint 默认不检测 d.ts 的文件
样式表
普通样式表,只要导入到组件就能使用
// Cannot find module './index.less' or its corresponding type declarations.
import './index.less';
// at src/typings.d.ts
// 把主流的样式表格式都加进去
declare module '*.css';
declare module '*.sass';
declare module '*.scss';
declare module '*.less';
declare module '*.styl';
declare module '*.stylus';
css-module
理论上按上面方式配置之后,css-module 方式导入也不会有报错,但是可以单独针对 css-module 引入的样式表模块做一下处理
// 在配置了 '*.less' 后,styles 是 any,没任何问题
import styles from './index.module.less';
// at src/typings.d.ts
// 把主流的样式表格式都加进去
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.less' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.styl' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.stylus' {
const classes: { [key: string]: string };
export default classes;
}
// 注意 module 声明应该在上面,ts 模块的匹配是优先匹配,如果符合 '*.less' 就不会再往下匹配了
declare module '*.css';
declare module '*.sass';
declare module '*.scss';
declare module '*.less';
declare module '*.styl';
declare module '*.stylus';
// 这样配置之后 styles 就是一个对象,类型是 { [key: string]: string }
图片和字体静态文件
这样类型的静态文件也类似,直接声明就是,以下两种方式都可以
// at src/typings.d.ts
// 声明导入的 '.png' 模块是一个字符串
declare module '*.png' {
const value: number;
export default value;
}
// 没有显式表明模块导出,就是 any
declare module '*.jpg';
declare module '*.jpeg';
window 上声明变量
不推荐在 window 上挂属性,不过有些变量是 html 主文档注入的,因而有些时候需要读 window 变量,但是直接读会报错,比如白龙马注入的环境变量
// Property 'ENV_TYPE' does not exist on type 'Window & typeof globalThis'.
window.ENV_TYPE;
// 在 src/typings.ts
// 注意不能写 export,利用 TypeScript 自动声明合并的能力
interface Window {
ENV_TYPE: 'prod' | 'pre' | 'local';
}
// 当前夸克和白龙马下面注入的变量主要有这几种
interface Window {
ENV_TYPE: 'prod' | 'pre' | 'local';
ucapi: {
invoke: Function;
};
__itrace: any;
}
常见的类型声明
获取一个变量的类型
TypeScript 中获取一个变量的类型可以使用 typeof 操作符,这种对于导入一个变量,要获取变量的类型十分方便
- 这里有两种情况,一是可以顺便把变量的类型也导入
- 但是变量的类型,不一定作为一个类型或者接口导出,例如下面的
case,要获取一个变量的类型,typeof很有用
interface User {
id: number;
name: string;
}
export const user = { id: 1, name: 'test' };
import { user } from './';
// error
// 'user' refers to a value, but is being used as a type here. Did you mean 'typeof user'?
const user2: user = { id: 2, name: 'test2' };
// ok
const user2: typeof user = { id: 2, name: 'test2' };
获取一个函数的返回类型
和获取变量类型一样,获取函数类型同样也存在于导入的函数中,可以方便的获取函数返回类型,在函数二次导出(例如 React 中的 useImperativeHandle 和 useRef ),只想获取函数的某个属性的类型特别有用
interface ProductInfo {
id: number;
price: number;
}
const usePay = () => {
const [productList, setProductList] = useState<ProductInfo[]>([]);
const buy = useCallback((id: number) => {}, []);
return { productList, buy };
};
// 在别的组件里面使用 `usePay` ,能正常识别状态
// const list: ProductInfo[]
const [list] = useState<ReturnType<typeof usePay>['productList']>([]);
useImperativeHandle 和 useRef 类型声明
-
useRef引用HTML元素const inputRef = useRef<HTMLInputElement>(null) // 使用 inputRef.current?.blur() // <input ref={inputRef} /> -
组件
forwardRef和useImperativeHandle类型声明function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef
& RefAttributes>;
forwardRef的签名有两个范型,T表示组件暴露的方法,P是组件的属性interface MessageModalProps { message: string; } export interface MessageModalMethods { open: () => void; close: () => void; } const MessageModal = forwardRef<MessageModalMethods, MessageModalProps>( ({ message }, ref) => { const [visible, setVisible] = useState<boolean>(false); useImperativeHandle(ref, () => ({ open: () => setVisible(true), close: () => setVisible(false), })); } ); -
useRef引用React组件import MessageModal from './message-modal'; import type { MessageModalMethods } from './message-modal'; const messageModalRef = useRef<MessageModalMethods>(null); // 使用 messageModalRef.current?.open(); messageModalRef.current?.close(); <MessageModal message={message} ref={messageModalRef} />;
JSDoc 生成属性说明
TypeScript 完整支持 JSDoc ,在 React 中可以使用 JSDoc 生成组件的属性说明,这样在提供公共组件的时候,用户在引用你的组件的时候,通过编辑器就能知道属性的注释是什么
// test.tsx
import { FC } from 'react';
// 注意不能是 //
interface TestProps {
/** 组件的值 */
val: string;
/** 组件点击回调 */
onClick?: () => void;
}
interface TestProps {
/**
* 组件的值
*/
val: string;
/** 组件点击回调 */
onClick?: () => void;
}
const Test: FC<TestProps> = ({ val, onClick, children }) => {
return <div></div>;
};
export default Test;
// 使用组件
<Test val={1} onClick={() => {}} />
// 当鼠标移到 val 上
(property) TestProps.val: string
组件的值
// 当鼠标移到 onClick 上
(property) TestProps.onClick?: () => void
组件点击回调
碰到的一些问题
关于原生组件一些方法的声明
对于原生的 html 标签,触发 onClick 或者其他事件的时候,有时候需要禁止事件的一些冒泡,或者禁止默认行为,这时候需要使用到类似于原生事件的 stopPropagation 的方法,但是如果不熟悉这里怎么定义,只能把组件回调参数 event 定义为 any
const onClick = (e: any) => {
e.stopPropagation();
};
这样写没有任何问题,不过没有任何语法提示就是了
其实 React 对各个 HTML 标签属性的类型声明做的非常好,当你在 div 声明一个 onClick 的时候,把鼠标移到上面,或者跳转到属性定义,就能看到 onClick 的函数声明
(property) React.DOMAttributes<HTMLDivElement>.onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
<div onClick={onClick}></div>
因而,我们的函数声明可以直接用 React.MouseEventHandler<HTMLDivElement>,同时,这时候的回调参数 e 会被识别为 React.MouseEvent<HTMLDivElement, MouseEvent>,你可以随意使用 React 合成事件里面的方法
(parameter) e: React.MouseEvent<HTMLDivElement, MouseEvent>
const onClick: React.MouseEventHandler<HTMLDivElement> = e => {
e.stopPropagation();
e.preventDefault();
};
当然,知道 e 的类型,也可以自己写,这样写也完全没有问题,看自己习惯了。
(parameter) e: React.MouseEvent<HTMLDivElement, MouseEvent>
const onClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
};
以上三种都能解决原生方法类型不对的问题,更加推荐 React.MouseEventHandler<HTMLDivElement> 的写法,因为这个有比较完整的提示
React.memo 和 React.FC
这两个结合起来本身是没什么问题的,可以看下面的例子
interface DrawerProps {
visible: boolean;
onClose: () => void;
closeWhenClickMask?: boolean;
zIndex?: number;
}
const Drawer: React.FC<DrawerProps> = ({ visible, onClose, children }) => {};
export default React.memo(Drawer);
// 在别的组件使用
// 正常
<Drawer visible={visible} onClose={() => {}}></Drawer>;
异常的是组件传了 children 的时候,报了下面的报错
Type '{ children: Element[]; visible: boolean; onClose: () => void; }' is not assignable to type 'IntrinsicAttributes & DrawerProps'.
Property 'children' does not exist on type 'IntrinsicAttributes & DrawerProps'.ts(2322)
// 异常
<Drawer visible={visible} onClose={() => {}}>
<div>121</div>
</Drawer>
这里的异常表明 children 不是 Drawer 的属性,但是正常情况下,React.FC 会注入 & { children?: React.ReactNode } 这个属性,但是这里好像凭空消失了一样,尝试了很多方法,最后总结出几种写法
// 手动把 children 属性补上,不建议
interface DrawerProps {
visible: boolean;
onClose: () => void;
closeWhenClickMask?: boolean;
zIndex?: number;
// 手动加上这个属性
children?: React.ReactNode;
}
// 手动更改导出的组件类型
export default React.memo(Drawer) 改成
export default React.memo(Drawer) as React.FC<DrawerProps>;
// 第三种,和第二种写法其实没有差别,不过将 React.memo 换在上面包裹了,改变了导出组件的类型
const Drawer: React.FC<DrawerProps> = React.memo(({ visible, onClose, children }) => {});
export default Drawer;
"jsx": "react-jsx" 的问题
"jsx": "react-jsx" 是 TypeScript 在 v4.1 之后加的一个属性,主要是给 React v17 使用的,如果不是 Ts 4.1+,这个属性会报错,如果碰到这个报错,请把 "typescript": "^4.1.0" 加到你的 package.json 里面,重新安装一次依赖,就不会报错了
Type 'Parameters' must have a 'Symbol.iterator' method that returns an iterator.
这个问题就有点怪了,Parameters<T> 返回的就是个数组,但是这里居然报错了,可以用 [...Parameters<T>] 包裹一下
/**
* 异步函数重试
* @param retryTimes 重试次数
* @param fn 需要重试执行的函数
* @param args 函数的参数
* @returns
*/
export const fnRetry = <T extends (...args: any) => any>(
retryTimes: number,
fn: T,
...args: [...Parameters<T>]
) => {
let times = retryTimes;
return new Promise<ReturnType<T>>((resolve, reject) => {
const run = () => {
fn(...args)
.then((r: any) => resolve(r))
.catch((err: any) => {
if (times > 0) {
times -= 1;
run();
} else {
reject(err);
}
});
};
run();
});
};