Typescript在React中的最佳实践
- 0
UI=fn(state)
我们的组件函数接收一个状态stage作为参数,函数的调用结果就是当前我们视图的UI,所以react最重要的就是fn和state这两部分、所以ts在react中最佳实践差不多等于在函数最佳实践,区别就是多了个jsx语法,TS开发确实比js开发多花时间去编写类型,但是后续维护,重构和代码提示方面确实收益大于话费的时间的。
目前的javascript项目基本都是webpack构建的,对于一个javascript项目我们迁移到 typescript 我们只需要以下几个重要步骤。
bash
在开发中我们难免用到第三方的npm模块,这些模块不一定是ts开发的,也不一定提供类型定义文件
我们可以安装对应的类型定义文件
@types/包https://www.typescriptlang.org/dt/search?search=
shell
tsc --init 生成tsconfig.json
https://www.typescriptlang.org/tsconfig
仅仅给idea提示用的
声明文件
装新包用ts写的一般都会提供声明文件,否则我们需要自己再
Package.json typings
有的项目没有自带声明文件,比如
react本身
https://www.typescriptlang.org/dt/search?search=
用@types/
@types/react
@types/react-dom
json
此时我们已经可以用tsc来编译我们的ts代码了,tsconfig知识给vscode
这一步我们主要修改webpack和babel的相关配置
js
对于typescript的编译我们主流方案有
编译方案 | 缺点 | 优点 |
---|---|---|
awesome-typescript-loader | 最近一次维护3年前 | |
ts-loader | es6语法无法转化、每次修改文件会重新去编译ts文件 | 类型校验加编译 |
babel/babel-loader | 没有类型检查功能,因此语法正确但无法通过 TypeScript 类型检查的代码可能会成功转换 部分语法无法编译常量枚举(7.15.0已支持) tsc --noEmit --watch 现有最佳方案 @babel/preset-typescript 使用 babel,不仅能处理 typescript,之前 babel 就已经存在的 polyfill 功能也能一并享受。并且由于 babel 只是移除类型注解节点,所以速度相当快。 | 不经过tsc直接转化成js,自带缓存,速度快 |
swc-loader | rust写的不会校验类型 | 速度快 |
esbuild/esbuild-loader | go写的 不会校验类型 | 速度快 |
没有类型检查功能,因此语法正确但无法通过 TypeScript 类型检查的代码可能会成功转换
tsc --noEmit --watch
目前最佳方案是使用babel-loader转化语法,使用tsc去检查类型
ts
inerface和type都可以定义类型,所以我们该用哪个,
表现上来说2者可以实现的功能都可以互相实现,唯一的声明合并方面,type不行即type 类型不能二次编辑,而 interface 可以随时扩展
在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口
在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type的约束性更强
ts
{}
和Record<string,any>
表示没有成员的对象
我们表示一个对象可以Record<string,any>
anyscript
使用any代表我们放弃了所有的类型检查,推荐使用Unknown
不缩小类型,我们无法对unknown 类型执行任何操作
unknown 和 any 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。
使用as
使用typeof
断言错了时语法能通过检测,但是运行的时候就会报错了!
JSX.Element 是React.createElement 或是转译 JSX 获得的对象的类型等价于ReactElement
JSX.Element React.createElement创建的jsx对象
React.ReactNode 所有可能返回值的集合,
他的声明如下,是一个联合类型
ts
我们可以把jsx赋值给他,在react世界里相当于any
这种方式的一个好处是获取到的是真实的类型声明, 导出的类型声明有时候是不完整的
ts
??
代替 ||
ts
现在重尚函数式编程,react也逐渐在向函数式编程思想迈进,当然这个函数式不是函数组件的意思,只是函数组件的编程思想运用到了函数式
一个标准的函数式组件是这样定义的
ts
props接口推荐以 ComponentName+props命名 ComponentNameProps
返回类型其实可以不写,ts可以自动推断我们的返回类型
tsx
很多人喜欢用React.FC定义类型,这是不推荐的做法,
https://github.com/facebook/create-react-app/pull/8177
React.FC 显式地定义了返回类型,其他方式是隐式推导的;如果返回类型是数组类型检查就会不通过提供子项的隐式定义,即使您的组件不需要有子项。
React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全;
React.FC 为 children 提供了隐式的类型(ReactNode)
比如以下就会报错
ts
children推荐用React.ReactNode定义
ts
react也为我们封装了一个类型工具
js
归根到底
ts
如果初始值已经可以说明类型,那么不用手动声明类型,TS 会自动推断类型
如果没有初始值需要给null初始值,需要给定类型
但是在使用时大家通常会加user?.id
通常没有初始值,需要初始化空对象,我们可以类型断言来骗ts编译器
ts
通常两种用法
js
如果我我们的ref不是有条件的渲染,divRef永远不会为空
js
一般的都是HTMLButtonElement类似的,如果实在不清楚我们可以写**HTMLElement,**编译器会提示我们正确的类型。
和useState差不多用法
ts
推断类型
ts
给定类型
ts
useEffect、useLayoutEffect一个参数,返回值必须函数或者undefined(void)
tsx
需要注意,自定义 Hook 的返回值如果是数组类型,TS 会自动推导为 所有Union 类型,而我们实际需要的是数组里里每一项的具体类型,需要手动添加 const 断言 进行处理:告诉ts这是个常量不会修改顺序删除
js
js
button有很多属性 type 我们要一个个加吗
可以用
tsx
如果要扩展属性也很方便使用 interface 或者 type
ts
这部分主要是处理点击事件以及input类似的声明,众所周知,使用箭头函数内联在html中每次渲染都会生成新的函数,所以对性能是有影响的,但是内联的函数能非常正确的推断我们的类型
js
事件类型
ts
剪切板事件对象:ClipboardEvent<T = Element>
拖拽事件对象:DragEvent<T = Element>
焦点事件对象:FocusEvent<T = Element>
表单事件对象:FormEvent<T = Element>
Change事件对象:ChangeEvent<T = Element>
键盘事件对象:KeyboardEvent<T = Element>
鼠标事件对象:MouseEvent<T = Element, E = NativeMouseEvent>
触摸事件对象:TouchEvent<T = Element>
滚轮事件对象:WheelEvent<T = Element>
动画事件对象:AnimationEvent<T = Element>
过渡事件对象:TransitionEvent<T = Element>
js
事件处理函数
ts
Axios react-query
Promise<T>
是一个泛型类型
ts
npm install typescript -D
npm install @types/react -D
npm install @types/react-dom -D
{
// ...
"compilerOptions": {
"incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
"tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
"diagnostics": true, // 打印诊断信息
"target": "ES5", // 目标语言的版本
"module": "CommonJS", // 生成代码的模板标准
"outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
"lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
"allowJS": true, // 允许编译器编译JS,JSX文件
"checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
"outDir": "./dist", // 指定输出目录
"rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
"declaration": true, // 生成声明文件,开启后会自动生成声明文件
"declarationDir": "./file", // 指定生成声明文件存放目录
"emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
"sourceMap": true, // 生成目标文件的sourceMap文件
"inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
"declarationMap": true, // 为声明文件生成sourceMap
"typeRoots": [], // 声明文件目录,默认时node_modules/@types
"types": [], // 加载的声明文件包
"removeComments":true, // 删除注释
"noEmit": true, // 不输出文件,即编译后不会生成任何js文件
"noEmitOnError": true, // 发送错误时不输出任何文件
"noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
"importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
"downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
"strict": true, // 开启所有严格的类型检查
"alwaysStrict": true, // 在代码中注入'use strict'
"noImplicitAny": true, // 不允许隐式的any类型
"strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
"strictFunctionTypes": true, // 不允许函数参数双向协变
"strictPropertyInitialization": true, // 类的实例属性必须初始化
"strictBindCallApply": true, // 严格的bind/call/apply检查
"noImplicitThis": true, // 不允许this有隐式的any类型
"noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
"noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
"noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
"noImplicitReturns": true, //每个分支都会有返回值
"esModuleInterop": true, // 允许export=导出,由import from 导入
"allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
"moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { // 路径映射,相对于baseUrl
// 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
"jquery": ["node_modules/jquery/dist/jquery.min.js"]
},
"rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
"listEmittedFiles": true, // 打印输出文件
"listFiles": true// 打印编译的文件(包括引用的声明文件)
}
}
import * as React from 'react'
import * as ReactDOM from 'react-dom'
//allowSyntheticDefaultImports
//tsconfig.json
{
"include": [
"src/**/*"
],
"compilerOptions": {
"target": "es5",
"module": "es6",
"noEmit": true,//不生成类型
"noImplicitAny": true, //不能有隐式any
"allowTernary": true,
"allowShortCircuit": true,
"allowSyntheticDefaultImports":true,//允许默认导入
"esModuleInterop": true,//esmodule支持
"strict": true,
"lib": [
"dom",
"es2015"//dom、html类型定义 es定义
],
"jsx": "react-jsx"//最重要
}
}
//webpack.config.js
const path = require('path');
module.exports = {
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
module: {
rules: [{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
}],
}
};
//png资源
declare module "*.png";
//扩展window对象
declare global {
interface Window {
MyVendorThing: MyVendorType;
}
}
interface Person {
age: number;
}
interface Person {
name: string;
}
type Person {
age: number;
}
type Person {
name: string;
}
//TS2300: Duplicate identifier 'Person'.
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
import { Button } from 'antd';
type ButtonProps = React.ComponentProps<typeof Button>;
const enum Gender {
female = '1',
male = '2',
}
function getSex(sex: string) {
if (sex === Gender.female) {
console.log(sex);
}
}
type PersonProps = {
name?: string;
age: number;
onClick: () => void;
};
function Person(props: PersonProps): JSX.Element {
const { name = '', age } = props;
return (
<div onClick={() => console.log('hello')}>
{name}-{age}
</div>
);
}
function App() {
const isShow = false;
return isShow ? <input type='text' /> : null;
}
const App: React.FC = props => props.children
const App: React.FC = () => [1, 2, 3]
const App: React.FC = () => 'hello'
interface ButtonProps {
children: React.ReactNode;
}
function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
type ButtonProps = {
onClick: () => void;
};
function Button(props: PropsWithChildren<ButtonProps>) {
const { children } = props;
return <button onClick={() => console.log('onClick')}>{children}</button>;
}
type AppProps = { message: string }; /* 也可用 interface */
const App = ({ message }: AppProps) => <div>{message}</div>; // 无大括号的箭头函数,利用 TS 推断。
//简单类型的初始值
const [val, setVal] = useState(false);
//复杂类型的初始值
const [user, setUser] = useState({ name: '张三', age: 18, password: '123456' });
const handleInitial =(val:typeof user) => {
setUser(val);
};
//一般情况下我们没有初始值,需要调用接口去请求数据
const [user, setUser] = useState<UserType | null>(null);//user?.id
const [user, setUser] = useState<UserType>({} as UserType);//better
function App() {
const divRef = useRef<HTMLDivElement>(null);//必须给null初始值,类型不用给null包括在里面
useLayoutEffect(() => {
if (!divRef.current) throw Error('divRef is not assigned');
divRef.current.innerHTML = 'hello world';
});
return <div ref={divRef}>hello</div>;
}
function App() {
const divRef = useRef<HTMLDivElement>(null!);
useLayoutEffect(() => {
divRef.current.innerHTML = 'hello world';
});
return <div ref={divRef}>hello</div>;
}
function App() {
const timer = useRef(0);
useLayoutEffect(() => {
timer.current = setTimeout(() => {
console.log('timer');
}, 1000);
return () => clearTimeout(timer.current);
});
return <div>hello</div>;
}
const AppContext = createContext({
authenticated: true,
lang: 'en',
theme: 'dark',
});
const MyComponent = () => {
const appContext = useContext(AppContext); //inferred as an object
return <h1>The current app language is {appContext.lang}</h1>;
};
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('dark');
const App = () => {
const theme = useContext(ThemeContext);
return <div>The theme is {theme}</div>;
};
const multiply = React.useCallback((value) => value * 2, []);//给类型
function useToggle() {
const [state, setState] = useState(false);
const toggle = () => setState(!state);
return [state, toggle];
}
function useToggle() {
const [state, setState] = useState(false);
const toggle = () => setState(!state);
return [state, toggle] as const;
}
type inputProps = ButtonHTMLAttributes<HTMLButtonElement>;
type inputProps = ComponentPropsWithoutRef<'button'>
function Button(props: inputProps) {
return <button {...props}>{props.children}</button>;
}
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
customProp: string;
}
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {
customProp: string;
};
function Button(props: ButtonProps) {
console.log(props.customProp);
return <button {...props}>{props.children}</button>;
}
function App() {
return (
<>
<button onClick={(e) => console.log(e)}>hello</button>
<input onChange={(e) => console.log(e)} />
</>
);
}
React.XXXEvent<HTMLXXXElement>
ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent
function App() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
console.log(e.target.value);
};
return <input onChange={handleChange} />;
}
React.ReactEventHandler<HTMLXXXElement>
function App() {
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
console.log(e.target.value);
};
return <input onChange={handleChange} />;
}
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
interface Res<T = unknown> {
code: number;
data: T;
message: string;
}
async function http<T>(config: AxiosRequestConfig): Promise<Res<T>> {
const instance = axios.create({
baseURL: process.env.BASE_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
validateStatus: ()=> true,
});
instance.interceptors.request.use(
(config) => {
return config;
},
(error) => {
return Promise.reject(error);
},
);
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
return Promise.reject(error);
},
);
const { data } = await instance.request<Res<T>>(config);
return data;
}
export default http;
export const getArticle = (params: ParamsType) => {
return http<Article>({
method: 'get',
url: '/article/page',
params,
});
};