前言
其实如果运用熟练的话,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 类型。
}
函数类型
type FunctionProps = {
/** 任意的函数类型 ? 不推荐 不能规定参数以及返回值类型 */
onSomething: Function;
/** 没有参数的函数 不需要返回值 😁 常用 */
onClick: () => void;
/** 带函数的参数 😁 非常常用 */
onChange: (id: number) => void;
/** 另一种函数语法 参数是 React 的按钮事件 😁 非常常用 */
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
/** 可选参数类型 😁 非常常用 */
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"]
}
类组件
// 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.FunctionComponent<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.FunctionComponent<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
幸运的是,当使用 Hook 时, TypeScript 类型推断工作得很好。这意味着你没有什么好担心的。举个例子:
// `value` is inferred as a string
// `setValue` is inferred as (newValue: string) => void
const [value, setValue] = useState('')
TypeScript 推断出 useState 钩子给出的值。这是一个 React 和 TypeScript 协同工作的成果。
在极少数情况下,你需要使用一个空值初始化 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);
?下面是一个使用 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;
}
}
可见,Hooks 并没有为 React 和 TypeScript 项目增加太多复杂性。
处理表单事件
最常见的情况之一是 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"/>
}
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('提交被触发');
};
扩展组件的 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 更具可读性,但最终取决于你和你的团队。
useRef
function Dog(){
const dogRef = useRef<HTMLDivElement>(null)
useEffect(() => {
console.log(dogRef.current);
}, [])
return (<div>
<div ref={dogRef}>dog</div>
</div>)
}
第三方库
无论是用于诸如 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 团队和社区共同维护。
|