目录
其实如果运用熟练的话,TS 只是在第一次开发的时候稍微多花一些时间去编写类型,后续维护、重构的时候就会发挥它神奇的作用了,还是非常推荐长期维护的项目使用它的。
先看几种定义 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
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;
- }
- 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元素相关的类型主要包括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>hellodiv> || <MyComp title="world" /> ||
- "abc" ||
- 123 ||
- true;
-
- // ReactElement和JSX.Element
- const b: React.ReactElement = <div>hello worlddiv> || <MyComp title="good" />;
-
- const c: JSX.Element = <MyComp title="good" /> || <div>hello worlddiv>;
原生的 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.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 Headingh1>
- }
-
- // Notice here we're using the function expression with the type OtherProps
- const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
- <h1>My Website Headingh1>
关于 interface 或 type ,我们建议遵循 react-typescript-cheatsheet 社区提出的准则:
让我们再看一个示例:
- 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>
- }
在此 组件中,我们为 Props 使用 type。每个 Props 上方都有简短的说明,以为其他开发人员提供更多背景信息。? 表示 Props 是可选的。children props 是一个 React.ReactNode 表示它还是一个 React 组件。
通常,在 React 和 TypeScript 项目中编写 Props 时,请记住以下几点:
在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 协同工作的成果。
在极少数情况下,你需要使用一个空值初始化 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
(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);
下面是一个使用 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;
- }
- }
- function Dog(){
- const dogRef = useRef<HTMLDivElement>(null)
- useEffect(() => {
- console.log(dogRef.current);
- }, [])
-
- return (<div>
- <div ref={dogRef}>dogdiv>
- div>)
- }
可见,Hooks 并没有为 React 和 TypeScript 项目增加太多复杂性。
如果我们需要仿照 useState 的形式,返回一个数组出去,则需要在返回值的末尾使用as const,标记这个返回值是个常量,否则返回的值将被推断成联合类型。
- const useInfo = () => {
- const [age, setAge] = useState(0);
-
- return [age, setAge] as const; // 类型为一个元组,[number, React.Dispatch
>] - };
在 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
) { - setValue(e.target.value)
- }
-
- return <input value={value} onChange={onChange} id="input-example"/>
- }
- // input输入框输入文字
- const handleInputChange = (evt: React.ChangeEvent
) => { - console.log(evt);
- };
-
- // button按钮点击
- const handleButtonClick = (evt: React.MouseEvent
) => { - console.log(evt);
- };
-
- // 移动端触摸div
- const handleDivTouch = (evt: React.TouchEvent
) => { - console.log(evt);
- };
| 事件类型 | 解释 |
|---|---|
ClipboardEvent | 剪切板事件对象 |
DragEvent | 拖拽事件对象 |
ChangeEvent | Change事件对象 |
KeyboardEvent | 键盘事件对象 |
MouseEvent | 鼠标事件对象 |
TouchEvent | 触摸事件对象 |
WheelEvent | 滚轮时间对象 |
AnimationEvent | 动画事件对象 |
TransitionEvent | 过渡事件对象 |
先处理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
) => { - setInputValue(event.target.value);
- };
-
- // 省略部分代码
-
- return (
- <div className="App">
- <input value={inputValue} onChange={handleInputChange} />
- div>
- );
- }
在上面的代码中需要注意的一点是,HTMLInputElement 特指HTML的输入标签。如果我们使用的是 textarea,我们将使用 HTMLTextAreaElement 来代替。
注意,MouseEvent 也是一个泛型,你可以在必要时对它进行限制。例如,让我们把上面的 MouseEvent 限制为专门从一个按钮发出的鼠标事件。
- const handleClick = (event: MouseEvent
) => { - console.log('提交被触发');
- };
有时,您希望获取为一个组件声明的 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 更具可读性,但最终取决于你和你的团队。
对于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/
-
- #npm
- npm install @types/
例如,如果您使用的是 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
{ - name: string;
- age: number;
- gender: T;
- }
-
- // 其他地方使用时
- const youngMan: IHuman
= { - name: 'zhangsan',
- age: 18,
- gender: 'male'
- }