目录
前言
其实如果运用熟练的话,TS 只是在第一次开发的时候稍微多花一些时间去编写类型,后续维护、重构的时候就会发挥它神奇的作用了,还是非常推荐长期维护的项目使用它的。
组件 Props
先看几种定义 Props 经常用到的类型:
基础类型
type BasicProps = {
message: string;
count: number;
disabled: boolean;
/** 数组类型 */
names: string[];
/** 用「联合类型」限制为下面两种「字符串字面量」类型 */
status: "waiting" | "success";
};
对象类型
type ObjectOrArrayProps = {
/** 如果你不需要用到具体的属性 可以这样模糊规定是个对象 ❌ 不推荐 */
obj: object;
obj2: {}; // 同上
/** 拥有具体属性的对象类型 ✅ 推荐 */
obj3: {
id: string;
title: string;
};
/** 对象数组 😁 常用 */
objArr: {
id: string;
title: string;
}[];
/** key 可以为任意 string,值限制为 MyTypeHere 类型 */
dict1: {
[key: string]: MyTypeHere;
};
dict2: Record<string, MyTypeHere>; // 基本上和 dict1 相同,用了 TS 内置的 Record 类型。
}
//通过接口定义相应的结构
interface Item {
name: string,
icon: string,
url: string,
status:boolean,
initShow:boolean,
copanyStatus:boolean
}
interface MyObject {
[key: string]: any;
}
函数类型
// 基本语法
interface InterfaceName {
(param1: parameterType1,param2:parameterType2... ): returnType;
}
// type定义
type FunctionProps = {
/** 任意的函数类型 ❌ 不推荐 不能规定参数以及返回值类型 */
onSomething: Function;
/** 没有参数的函数 不需要返回值 😁 常用 */
onClick: () => void;
/** 带函数的参数 😁 非常常用 */
onChange: (id: number) => void;
/** 另一种函数语法 参数是 React 的按钮事件 😁 非常常用 */
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
(name:string):string;
/** 可选参数类型 😁 非常常用 */
optional?: OptionalType;
}
React 相关类型
export declare interface AppProps {
children1: JSX.Element; // ❌ 不推荐 没有考虑数组
children2: JSX.Element | JSX.Element[]; // ❌ 不推荐 没有考虑字符串 children
children4: React.ReactChild[]; // 稍微好点 但是没考虑 null
children: React.ReactNode; // ✅ 包含所有 children 情况
functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点的函数
style?: React.CSSProperties; // ✅ 推荐 在内联 style 时使用
// ✅ 推荐原生 button 标签自带的所有 props 类型
// 也可以在泛型的位置传入组件 提取组件的 Props 类型
props: React.ComponentProps<"button">;
// ✅ 推荐 利用上一步的做法 再进一步的提取出原生的 onClick 函数类型
// 此时函数的第一个参数会自动推断为 React 的点击事件类型
onClickButton:React.ComponentProps<"button">["onClick"]
}
React元素相关
React元素相关的类型主要包括ReactNode、ReactElement、JSX.Element。
ReactNode。表示任意类型的React节点,这是个联合类型,包含情况众多;
ReactElement/JSX。从使用表现上来看,可以认为这两者是一致的,属于ReactNode的子集,表示“原生的DOM组件”或“自定义组件的执行结果”。
使用示例如下:
const MyComp: React.FC<{ title: string; }> = ({title}) => <h2>{title}</h2>;
// ReactNode
const a: React.ReactNode =
null ||
undefined || <div>hello</div> || <MyComp title="world" /> ||
"abc" ||
123 ||
true;
// ReactElement和JSX.Element
const b: React.ReactElement = <div>hello world</div> || <MyComp title="good" />;
const c: JSX.Element = <MyComp title="good" /> || <div>hello world</div>;
原生DOM相关
原生的 DOM 相关的类型,主要有以下这么几个:Element、 HTMLElement、HTMLxxxElment。
简单来说: Element = HTMLElement + SVGElement。
SVGElement一般开发比较少用到,而HTMLElement却非常常见,它的子类型包括HTMLDivElement、HTMLInputElement、HTMLSpanElement等等。
因此我们可以得知,其关系为:Element > HTMLElement > HTMLxxxElement,原则上是尽量写详细。
类组件
// Second.tsx
import * as React from 'react'
import SecondComponent from './component/Second1'
export interface ISecondProps {}
export interface ISecondState {
count: number
title: string
}
export default class Second extends React.Component<
ISecondProps,
ISecondState
> {
constructor(props: ISecondProps) {
super(props)
this.state = {
count: 0,
title: 'Second标题',
}
this.changeCount = this.changeCount.bind(this)
}
changeCount() {
let result = this.state.count + 1
this.setState({
count: result,
})
}
public render() {
return (
<div>
{this.state.title}--{this.state.count}
<button onClick={this.changeCount}>点击增加</button>
<SecondComponent count={this.state.count}></SecondComponent>
</div>
)
}
}
// second1.tsx
import * as React from 'react'
export interface ISecond1Props {
count: number
}
export interface ISecond1State {
title: string
}
export default class Second1 extends React.Component<
ISecond1Props,
ISecond1State
> {
constructor(props: ISecond1Props) {
super(props)
this.state = {
title: '子组件标题',
}
}
public render() {
return (
<div>
{this.state.title}---{this.props.count}
</div>
)
}
}
函数组件
// Home.tsx
import * as React from 'react'
import { useState, useEffect } from 'react'
import Home1 from './component/Home1'
interface IHomeProps {
childcount: number;
}
const Home: React.FC<IHomeProps> = (props) => {
const [count, setCount] = useState<number>(0)
function addcount() {
setCount(count + 1)
}
return (
<div>
<span>Home父组件内容数字是{count}</span>
<button onClick={addcount}>点击增加数字</button>
<Home1 childcount={count}></Home1>
</div>
)
}
export default Home
// Home1.tsx
import * as React from 'react'
interface IHome1Props {
childcount: number;
}
const Home1: React.FC<IHome1Props> = (props) => {
const { childcount } = props
return <div>Home组件1--{childcount}</div>
}
export default Home1
import React from 'react'
interface Props {
name: string;
color: string;
}
type OtherProps = {
name: string;
color: string;
}
// Notice here we're using the function declaration with the interface Props
function Heading({ name, color }: Props): React.ReactNode {
return <h1>My Website Heading</h1>
}
// Notice here we're using the function expression with the type OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
<h1>My Website Heading</h1>
关于 interface 或 type ,我们建议遵循 react-typescript-cheatsheet 社区提出的准则:
- 在编写库或第三方环境类型定义时,始终将 interface 用于公共 API 的定义。
- 考虑为你的 React 组件的 State 和 Props 使用 type ,因为它更受约束。”
让我们再看一个示例:
import React from 'react'
type Props = {
/** color to use for the background */
color?: string;
/** standard children prop: accepts any valid React Node */
children: React.ReactNode;
/** callback function passed to the onClick handler*/
onClick: () => void;
}
const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
return <button style={{ backgroundColor: color }} onClick={onClick}>{children}</button>
}
在此 <Button /> 组件中,我们为 Props 使用 type。每个 Props 上方都有简短的说明,以为其他开发人员提供更多背景信息。? 表示 Props 是可选的。children props 是一个 React.ReactNode 表示它还是一个 React 组件。
通常,在 React 和 TypeScript 项目中编写 Props 时,请记住以下几点:
- 始终使用 TSDoc 标记为你的 Props 添加描述性注释 /** comment */。
- 无论你为组件 Props 使用 type 还是 interfaces ,都应始终使用它们。
- 如果 props 是可选的,请适当处理或使用默认值。
与hooks的结合
在hooks中,并非全部钩子都与TS有强关联,比如useEffect就不依赖TS做类型定义,我们挑选比较常见的几个和TS强关联的钩子来看看。
// `value` is inferred as a string
// `setValue` is inferred as (newValue: string) => void
const [value, setValue] = useState('')
TypeScript 推断出 useState 钩子给出的值。这是一个 React 和 TypeScript 协同工作的成果。
useState
在极少数情况下,你需要使用一个空值初始化 Hook ,可以使用泛型并传递联合以正确键入 Hook 。查看此实例:
type User = {
email: string;
id: string;
}
// the generic is the < >
// the union is the User | null
// together, TypeScript knows, "Ah, user can be User or null".
const [user, setUser] = useState<User | null>(null);
1、如果初始值能说明类型,就不用给 useState 指明泛型变量;
// ❌这样写是不必要的,因为初始值0已经能说明count类型
const [count, setCount] = useState<number>(0);
// ✅这样写好点
const [count, setCount] = useState(0);
2、如果初始值是 null 或 undefined,那就要通过泛型手动传入你期望的类型,并在访问属性的时候通过可选链来规避语法错误。
interface IUser {
name: string;
age: number;
}
const [user, setUser] = React.useState<IUser | null>(null);
console.log(user?.name);
import React, {useState, useEffect} from "react"
interface User{
id: number;
name: string;
email: string;
}
const App: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users").then(response => response.json()).then(data => setUsers(data));
}, [])
return <div>
<h2>用户列表</h2>
<ul>
{users.map(user => (<li key={user.id}>{user.name}-{user.email}</li>))}
</ul>
</div>
}
export default App;
userReducer
下面是一个使用 userReducer 的例子:
type AppState = {};
type Action =
| { type: "SET_ONE"; payload: string }
| { type: "SET_TWO"; payload: number };
export function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
case "SET_ONE":
return {
...state,
one: action.payload // `payload` is string
};
case "SET_TWO":
return {
...state,
two: action.payload // `payload` is number
};
default:
return state;
}
}
useRef
// App.tsx
import React, { useRef } from "react";
import "./App.css";
type AppProps = {
message: string;
};
const App: React.FC<AppProps> = ({ message }) => {
const myRef = useRef<HTMLInputElement>(null);
return (
<div>
<input ref={myRef} />
<button
onClick={() => console.log((myRef.current as HTMLInputElement).value)}
>
{message}
</button>
</div>
);
};
export default App;
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App message="click"/>
</React.StrictMode>
);
reportWebVitals();
可见,Hooks 并没有为 React 和 TypeScript 项目增加太多复杂性。
自定义 hook
如果我们需要仿照 useState 的形式,返回一个数组出去,则需要在返回值的末尾使用as const,标记这个返回值是个常量,否则返回的值将被推断成联合类型。
const useInfo = () => {
const [age, setAge] = useState(0);
return [age, setAge] as const; // 类型为一个元组,[number, React.Dispatch<React.SetStateAction<number>>]
};
React合成事件相关
在 React 中,原生事件被处理成了React 事件,其内部是通过事件委托来优化内存,减少DOM事件绑定的。言归正传,React 事件的通用格式为[xxx]Event,常见的有MouseEvent、ChangeEvent、TouchEvent,是一个泛型类型,泛型变量为触发该事件的 DOM 元素类型。
最常见的情况之一是 onChange 在表单的输入字段上正确键入使用的。这是一个例子:
import React from 'react'
const MyInput = () => {
const [value, setValue] = React.useState('')
// 事件类型是“ChangeEvent”
// 我们将 “HTMLInputElement” 传递给 input
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}
return <input value={value} onChange={onChange} id="input-example"/>
}
// input输入框输入文字
const handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
console.log(evt);
};
// button按钮点击
const handleButtonClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
console.log(evt);
};
// 移动端触摸div
const handleDivTouch = (evt: React.TouchEvent<HTMLDivElement>) => {
console.log(evt);
};
Event 事件对象类型
事件类型 | 解释 |
---|---|
ClipboardEvent<T = Element> | 剪切板事件对象 |
DragEvent<T =Element> | 拖拽事件对象 |
ChangeEvent<T = Element> | Change事件对象 |
KeyboardEvent<T = Element> | 键盘事件对象 |
MouseEvent<T = Element> | 鼠标事件对象 |
TouchEvent<T = Element> | 触摸事件对象 |
WheelEvent<T = Element> | 滚轮时间对象 |
AnimationEvent<T = Element> | 动画事件对象 |
TransitionEvent<T = Element> | 过渡事件对象 |
先处理onClick事件。React 提供了一个 MouseEvent 类型,可以直接使用:
import {
useState,
MouseEvent,
} from 'react';
export default function App() {
// 省略部分代码
const handleClick = (event: MouseEvent) => {
console.log('提交被触发');
};
return (
<div className="App">
<button onClick={handleClick}>提交</button>
</div>
);
}
onClick 事件实际上是由React维护的:它是一个合成事件。
合成事件是React对浏览器事件的一种包装,以便不同的浏览器,都有相同的API。
handleInputChange函数与 handleClick 非常相似,但有一个明显的区别。不同的是,ChangeEvent 是一个泛型,你必须提供什么样的DOM元素正在被使用。
import {
useState,
ChangeEvent
} from 'react';
export default function App() {
const [inputValue, setInputValue] = useState('');
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
// 省略部分代码
return (
<div className="App">
<input value={inputValue} onChange={handleInputChange} />
</div>
);
}
在上面的代码中需要注意的一点是,HTMLInputElement 特指HTML的输入标签。如果我们使用的是 textarea,我们将使用 HTMLTextAreaElement 来代替。
注意,MouseEvent 也是一个泛型,你可以在必要时对它进行限制。例如,让我们把上面的 MouseEvent 限制为专门从一个按钮发出的鼠标事件。
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
console.log('提交被触发');
};
styles
// utils.d.ts
declare interface StyleProps {
style?: React.CSSProperties
className?: string
}
// Button.tsx
interface ButtonProps extends StyleProps {
label: string
}
const Button = ({ label, ...styleProps }: ButtonProps) => (
<button {...styleProps}>{label}</button>
)
扩展组件的 Props
有时,您希望获取为一个组件声明的 Props,并对它们进行扩展,以便在另一个组件上使用它们。但是你可能想要修改一两个属性。还记得我们如何看待两种类型组件 Props、type 或 interfaces 的方法吗?取决于你使用的组件决定了你如何扩展组件 Props 。让我们先看看如何使用 type:
import React from 'react';
type ButtonProps = {
/** the background color of the button */
color: string;
/** the text to show inside the button */
text: string;
}
type ContainerProps = ButtonProps & {
/** the height of the container (value used with 'px') */
height: number;
}
const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}
如果你使用 interface 来声明 props,那么我们可以使用关键字 extends 从本质上“扩展”该接口,但要进行一些修改:
import React from 'react';
interface ButtonProps {
/** the background color of the button */
color: string;
/** the text to show inside the button */
text: string;
}
interface ContainerProps extends ButtonProps {
/** the height of the container (value used with 'px') */
height: number;
}
const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}
两种方法都可以解决问题。由您决定使用哪个。就个人而言,扩展 interface 更具可读性,但最终取决于你和你的团队。
redux相关
对于action的定义,我们可以使用官方暴露的AnyAction,放宽对于action内部键值对的限制,如下:
import { AnyAction } from "redux";
const DEF_STATE = {
count: 0,
type: 'integer'
};
// 使用redux的AnyAction放宽限制
function countReducer(state = DEF_STATE, action: AnyAction) {
switch (action.type) {
case "INCREASE_COUNT":
return {
...state,
count: state.count + 1,
};
case "DECREASE_COUNT":
return {
...state,
count: state.count - 1,
};
default:
return state;
}
}
export default countReducer;
第三方库
无论是用于诸如 Apollo 之类的 GraphQL 客户端还是用于诸如 React Testing Library 之类的测试,我们经常会在 React 和 TypeScript 项目中使用第三方库。发生这种情况时,你要做的第一件事就是查看这个库是否有一个带有 TypeScript 类型定义 @types 包。你可以通过运行:
#yarn
yarn add @types/<package-name>
#npm
npm install @types/<package-name>
例如,如果您使用的是 Jest ,则可以通过运行以下命令来实现:
#yarn
yarn add @types/jest
#npm
npm install @types/jest
这样,每当在项目中使用 Jest 时,就可以增加类型安全性。
该 @types 命名空间被保留用于包类型定义。它们位于一个名为 DefinitelyTyped 的存储库中,该存储库由 TypeScript 团队和社区共同维护。
规约
子组件的入参命名为[组件名]Props,如:
// 比如当前组件名为InfoCard
export interface InfoCardProps {
name: string;
age: number;
}
2、interface接口类型以大写开头;
3、为后端接口的出入参书写interface,同时使用利于编辑器提示的jsdoc风格做注释,如:
export interface GetUserInfoReqParams {
/** 名字 */
name: string;
/** 年龄 */
age: number;
/** 性别 */
gender: string;
}
其他
键名或键值不确定如何处理?
// 表示键名不确定,键值限制为number类型
export interface NotSureAboutKey {
[key: string]: number;
}
// 当键名键值都不确定时,以下接口对任何对象都是适用的
export interface AllNotSure {
[key: string]: any;
}
如何在接口中使用泛型变量?
所谓泛型,就是预定义类型。它的目的是:达到类型定义的局部灵活,提高复用性。我们通常会在接口中使用泛型,如:
// 通常,我们会为接口的泛型变量指定一个默认类型
interface IHuman<T = unknown> {
name: string;
age: number;
gender: T;
}
// 其他地方使用时
const youngMan: IHuman<string> = {
name: 'zhangsan',
age: 18,
gender: 'male'
}
NodeListOf使用:
const onChangeWidth = () => {
const elements = document.querySelectorAll(".menu-item dd") as NodeListOf<
HTMLElement
>;
if (status) {
mobxData.setWidth(90);
for (let i = 0; i < elements.length; i++) {
(elements[i].style as any).display = "block";
}
} else {
mobxData.setWidth(60);
for (let i = 0; i < elements.length; i++) {
(elements[i].style as any).display = "none";
}
}
setStatus(!status);
};