首页 前端知识 React TypeScript 实践

React TypeScript 实践

2024-06-04 10:06:34 前端知识 前端哥 797 864 我要收藏

主要内容包括准备知识、如何引入 React、函数式组件的声明方式、Hooks、useRef<T>、useEffect、useMemo<T> / useCallback<T>、自定义 Hooks、默认属性 defaultProps、Types or Interfaces、获取未导出的 Type、Props、常用 Props ts 类型、常用 React 属性类型、Forms and Events、onSubmit、Operators、Tips、不要在 type 或 interface 中使用函数声明、事件处理、Promise 类型、泛型参数的组件、什么时候使用泛型、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

准备知识

  • 熟悉 React
  • 熟悉 TypeScript (参考书籍:2ality’s guide[1], 初学者建议阅读:chibicode’s tutorial[2])
  • 熟读 React 官方文档 TS 部分[3]
  • 熟读 TypeScript playground React 部分[4]

验证环境

"umi": "^4.0.63"
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"typescript": "^5.0.0"

如何引入 React

import * as React from 'react'
import * as ReactDOM from 'react-dom'

这种引用方式被证明[5]是最可靠的一种方式, 推荐使用

而另外一种引用方式:

import React from 'react'
import ReactDOM from 'react-dom'

需要添加额外的配置:“allowSyntheticDefaultImports”: true (项目中虽然没手动加,在umi项目的./src/.umi/tsconfig.json中可以看到有这个配置项)

函数式组件的声明方式

第一种:也是比较推荐的一种,使用 React.FunctionComponent,简写形式:React.FC:

// Great
type AppProps = {
  message: string,
  children?: React.ReactNode	//React18以后需要加children属性
}

const App: React.FC<AppProps> = ({ message, children }) => (
  <div>
    {message}
    {children}
  </div>
)

需要注意的是React18以前不需要显式指定children属性,而根据官方文档,React18需要明确列出:

image

React17中**@types/react**的对FC的定义,帮我们默认声明了:

    interface FunctionComponent<P = {}> {
        (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
        propTypes?: WeakValidationMap<P> | undefined;
        contextTypes?: ValidationMap<any> | undefined;
        defaultProps?: Partial<P> | undefined;
        displayName?: string | undefined;
    }

	type PropsWithChildren<P> = P & { children?: ReactNode | undefined };

React18中**@types/react**的对FC的定义,取消了默认声明:

    interface FunctionComponent<P = {}> {
        (props: P, context?: any): ReactElement<any, any> | null;
        propTypes?: WeakValidationMap<P> | undefined;
        contextTypes?: ValidationMap<any> | undefined;
        defaultProps?: Partial<P> | undefined;
        displayName?: string | undefined;
    }

使用 React.FC 声明函数组件和普通声明以及 PropsWithChildren 的区别是:

  • React.FC 显式地定义了返回类型,其他方式是隐式推导的
  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全
  • React.FC 为 children 提供了隐式的类型(ReactElement | null),但是目前,提供的类型存在一些 issue[6](问题)

比如以下用法 React.FC 会报类型错误:

const App: React.FC<{children?: React.ReactNode}> = props => props.children
const App: React.FC = () => [1, 2, 3]
const App: React.FC = () => 'hello'

实测截图:

ts-4.png

ts-2.png

ts-3.png

解决方法:

const App: React.FC<{children?: React.ReactNode}> = props => props.children as any
const App: React.FC<{}> = () => [1, 2, 3] as any
const App: React.FC<{}> = () => 'hello' as any
// 或者
const App: React.FC<{children?: React.ReactNode}> = props => (props.children as unknown) as JSX.Element
const App: React.FC<{}> = () => ([1, 2, 3] as unknown) as JSX.Element
const App: React.FC<{}> = () => ('hello' as unknown) as JSX.Element

在通常情况下,使用 React.FC 的方式声明最简单有效,推荐使用;如果出现类型不兼容问题,建议使用以下两种方式:

第二种:使用 PropsWithChildren,这种方式可以为你省去频繁定义 children 的类型,自动设置 children 类型为 ReactNode:

type AppProps = React.PropsWithChildren<{ message: string }>
const App = ({ message, children }: AppProps) => (
  <div>
    {message}
    {children}
  </div>
)

第三种:直接声明:

type AppProps = {
  message: string
  children?: React.ReactNode
}

const App = ({ message, children }: AppProps) => (
  <div>
    {message}
    {children}
  </div>
)

Hooks

useState<T>

大部分情况下,TS 会自动为你推导 state 的类型:

// `val`会推导为boolean类型, toggle接收boolean类型参数
const [val, toggle] = React.useState(false)
// obj会自动推导为类型: {name: string}
const [obj] = React.useState({ name: 'sj' })
// arr会自动推导为类型: string[]
const [arr] = React.useState(['One', 'Two'])

使用推导类型作为接口/类型:

export default function App() {
  // user会自动推导为类型: {name: string}
  const [user] = React.useState({ name: 'sj', age: 32 });
	//typeof user自动推导类型
  const showUser = React.useCallback((obj: typeof user) => {
    return `My name is ${obj.name}, My age is ${obj.age}`;
  }, []);
  
  return <div className='App'>用户: {showUser(user)}</div>;
}

但是,一些状态初始值为空时(null),需要显示地声明类型:

  const [user, setUser] = React.useState(null)

  const showUser = React.useCallback((obj: typeof user) => {
    return `My name is ${obj.name}, My age is ${obj.age}`;	//ts报错:“obj”可能为 “null”。
  }, []);

//改成显示声明
type User = {
  name: string
  age: number
}		

  const [user, setUser] = React.useState<User|null>(null)

  const showUser = React.useCallback((obj: typeof user) => {
	//obj可能是null,需要判断下
    if (null === obj) return '--';
    return `My name is ${obj.name}, My age is ${obj.age}`;
  }, []);

useRef<T>

当初始值为 null 时,有两种创建方式:

const ref1 = React.useRef<HTMLInputElement>(null)
const ref2 = React.useRef<HTMLInputElement | null>(null)

这两种的区别在于

  • 第一种方式的 ref1.current 是只读的(read-only),我们无法修改current值,但是可以传递给内置的 ref 属性,绑定 DOM 元素 
  • 第二种方式的 ref2.current 是可变的(类似于声明类的成员变量)

为了验证只读属性,将HTMLInputElement改成number:

  const ref1 = React.useRef<number>(null);
  const ref2 = React.useRef<number | null>(null);

  ref1.current = 1;	//ts报错:无法为“current”赋值,因为它是只读属性。
  ref2.current = 2;

但是如果初始值不为null,那ref.current是可变的:

//写不写<number>都可以,跟useState一样也有类型推导
const ref = React.useRef(0)
React.useEffect(() => {
  ref.current += 1
}, [])

这两种方式在使用时,都需要对类型进行检查:

const onButtonClick = () => {
  ref1.current?.focus()
  ref2.current?.focus()
}

在某种情况下,可以省去类型检查,通过添加 ! 断言不推荐

// Bad
function MyComponent() {
  const ref1 = React.useRef<HTMLDivElement>(null!);	//null!断言
  React.useEffect(() => {
    //  不需要做类型检查,需要人为保证ref1.current.focus一定存在
    doSomethingWith(ref1.current.focus())
  })
  return <div ref={ref1}> etc </div>
}

useEffect

useEffect 需要注意回调函数的返回值只能是函数或者 undefined

function App() {
  // undefined作为回调函数的返回值
  React.useEffect(() => {
    // do something...
  }, []);
  // 返回值是一个函数
  React.useEffect(() => {
    // do something...
    return () => {};
  }, []);
}

useMemo<T> / useCallback<T>

useMemo 和 useCallback 都可以直接从它们返回的值中推断出它们的类型。

useCallback 的参数必须指定类型,否则 ts 会报错,默认指定为 any:

  const value = 10, multiplier = 20;
  // 自动推断result返回值为 number
  const result = React.useMemo(() => value * 2, [value]);
  // 自动推断multiply 类型为 (value: number) => number
  const multiply = React.useCallback(
    (value: number) => value * multiplier,
    [multiplier]
  );

同时也支持传入泛型, useMemo 的泛型指定了返回值类型useCallback 的泛型指定了参数类型

  // useMemo也可以显式的指定返回值类型,返回值不一致会报错
  //类型“() => number”的参数不能赋给类型“() => string”的参数。
  const result = React.useMemo<string>(() => 2, []);  //ts报错:不能将类型“number”分配给类型“string”。

	//传入的React.ChangeEventHandler<HTMLInputElement>指定了evt的类型
  const handleChange1 = React.useCallback<
    React.ChangeEventHandler<HTMLInputElement>
  >((evt) => {
    console.log(evt.target.value);
  }, []);
	//等同于(自己推断的,未经过实际项目验证)
  const handleChange2 = React.useCallback(
    (evt: React.ChangeEvent<HTMLInputElement>) => {
      console.log(evt.target.value);
    },
    []
  );
	

自定义 Hooks

需要注意,自定义 Hook 的返回值如果是数组类型,TS 会自动推导为 Union (联合)类型,而我们实际需要的是数组里里每一项的具体类型,需要手动添加 const 断言 进行处理:

//未使用as const推断的类型:function useLoading(): (boolean | ((aPromise: Promise<any>) => Promise<void>))[]
//使用as const后推断的类型:function useLoading(): readonly [boolean, (aPromise: Promise<any>) => Promise<void>]
function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.then(() => setState(false));
  };
  // 实际需要: [boolean, typeof load] 类型
  // 而不是自动推导的:(boolean | typeof load)[]
  return [isLoading, load] as const;
}

如果使用 const 断言遇到问题[7],也可以直接定义返回类型:

export function useLoading(): [
  boolean,
  (aPromise: Promise<any>) => Promise<any>
] {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.then(() => setState(false));
  };
  return [isLoading, load];
}

如果有大量的自定义 Hook 需要处理,这里有一个方便的工具方法可以处理 数组 返回值:

//实际上就是把参数数组直接返回
function tuplify<T extends any[]>(...elements: T) {
  return elements;
}

//function useLoading(): (boolean | ((aPromise: Promise<any>) => Promise<void>))[]
function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.then(() => setState(false));
  };

  // (boolean | typeof load)[]
  return [isLoading, load];
}

//function useTupleLoading(): [boolean, (aPromise: Promise<any>) => Promise<void>]
function useTupleLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.then(() => setState(false));
  };

  // [boolean, typeof load]
  return tuplify(isLoading, load);
}

默认属性 defaultProps

大部分文章都不推荐使用 defaultProps , 相关讨论可以点击参考链接[8]

推荐方式:使用默认参数值来代替默认属性:

type GreetProps = { age?: number }
const Greet = ({ age = 21 }: GreetProps) => {
  /* ... */
}

defaultProps 类型

TypeScript3.0+[9] 在默认属性 的类型推导上有了极大的改进,虽然尚且存在一些边界 case 仍然存在问题[10]不推荐使用,如果有需要使用的场景,可参照如下方式:

type IProps = {
  name: string;
};
const defaultProps = {
  age: 25,
};

// 类型定义, 合并类型
type GreetProps = IProps & typeof defaultProps;
const Greet = (props: GreetProps) => (
  <div>
    {props.age}
    {props.name}
  </div>
);
//写不写好像都可以,TS都没有报错,TS版本号^5.0.0
Greet.defaultProps = defaultProps;

// 使用 将Gree的默认类型作为类型定义传递
const TestComponent = (props: React.ComponentProps<typeof Greet>) => {
  return (
    <div>
      {props.age}
      {props.name}
    </div>
  );
};

const el = <TestComponent name='foo' age={11} />;

Types or Interfaces

在日常的 react 开发中 interface 和 type 的使用场景十分类似

implements 与 extends 静态操作,不允许存在一种或另一种实现的情况,所以不支持使用联合类型:

class Point {
  x: number = 2
  y: number = 3
}
interface IShape {
  area(): number
}
type Perimeter = {
  perimeter(): number
}
//联合类型,既可以是IShape,也可以是Perimeter,类型未知
type RectangleShape = (IShape | Perimeter) & Point;

class Rectangle implements RectangleShape {
  // TS报错:类只能实现具有静态已知成员的对象类型或对象类型的交集。
  x = 2
  y = 3
  area() {
    return this.x + this.y
  }
}

//TS报错: 接口只能扩展使用静态已知成员的对象类型或对象类型的交集。
interface ShapeOrPerimeter extends RectangleShape {}

使用 Type 还是 Interface?

有几种常用规则:

  • 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口
  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type的约束性更强

interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:

  • type 类型不能二次编辑,而 interface 可以随时扩展
interface Animal {
  name: string;
}

// 可以继续在原有属性基础上,添加新属性:color
interface Animal {
  color: string;
}
/********************************/
type Animal = {
  name: string;
};
// type类型不支持属性扩展
// Error: Duplicate identifier 'Animal'
type Animal = {
  color: string;
};

获取未导出的 Type

某些场景下我们在引入第三方的库时会发现想要使用的组件并没有导出我们需要的组件参数类型或者返回值类型,这时候我们可以通过 ComponentProps/ ReturnType 来获取到想要的类型。

// 获取参数类型
import { Button } from 'antd' // 但是未导出props type
type ButtonProps = React.ComponentProps<typeof Button> // 获取props
type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick属性,Omit:TS提供的方法,去除类型中某些项

//即AlertButton用了Button的属性,但是又去除了onClick
const AlertButton: React.FC<AlertButtonProps> = props => (
  <Button onClick={() => alert('hello')} {...props} />
)

const AlertButton2 = () => (
  //TS报错:不能将类型“{ onClick: () => void; }”分配给类型“IntrinsicAttributes & AlertButtonProps”。
  //类型“IntrinsicAttributes & AlertButtonProps”上不存在属性“onClick”。
  <AlertButton onClick={ () => alert('hello')}/>
)
// 获取返回值类型
function foo() {
  return { baz: 1 };
}
type FooReturn = ReturnType<typeof foo>; // { baz: number }

Props

通常我们使用 type 来定义 Props,为了提高可维护性和代码可读性,在日常的开发过程中我们希望可以添加清晰的注释。

现在有这样一个 type:

type OtherProps = {
  name: string;
  color: string;
};

const OtherHeading: React.FC<OtherProps> = ({ name, color }) => (
  <h1>My Website Heading</h1>
);

在使用的过程中,鼠标移到对应类型会有如下展示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HyXwVetF-1688693203724)(https://s2.loli.net/2023/07/06/Soi185ZQ2pGUDBC.png)]

ts-hover-2.png

增加相对详细的注释,使用时会更清晰,需要注意,注释需要使用 /**/  // 无法被 vscode 识别

/**
 * @param color color param description
 * @param children children param description
 * @param onClick onClick param description
 */
type Props = {
  /** color description */
  color?: string
  /** children description*/
  children: React.ReactNode
  /** onClick description*/
  onClick: () => void
}

const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
  return (
    <button style={{ backgroundColor: color }} onClick={onClick}>
      {children}
    </button>
  )
}

鼠标再次移到对应类型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jfBt8zlP-1688693203726)(https://s2.loli.net/2023/07/06/HshnAwpJMjF4zVB.png)]

ts-hover-4.png

常用 Props ts 类型

基础属性类型

//MyTypeHere、OptionalType为任意定义的类型
type AppProps = {
  message: string
  count: number
  disabled: boolean
  /** array of a type! */
  names: string[]
  /** string literals to specify exact string values, with a union type to join them together */
  status: 'waiting' | 'success'
  /** 任意需要使用其属性的对象(不推荐使用,但是作为占位很有用) */
  obj: object
  /** 作用和`object`几乎一样,和 `Object`完全一样 */
  obj2: {}
  /** 列出对象全部数量的属性 (推荐使用) */
  obj3: {
    id: string
    title: string
  }
  /** array of objects! (common) */
  objArr: {
    id: string
    title: string
  }[]
  /** 任意数量属性的字典,具有相同类型*/
  dict1: {
    [key: string]: MyTypeHere
  }
  /** 作用和dict1完全相同 */
  dict2: Record<string, MyTypeHere>
  /** 任意完全不会调用的函数 */
  onSomething: Function
  /** 没有参数&返回值的函数 */
  onClick: () => void
  /** 携带参数的函数 */
  onChange: (id: number) => void
  /** 携带点击事件的函数 */
  onClick(event: React.MouseEvent<HTMLButtonElement>): void
  /** 可选的属性 */
  optional?: OptionalType
}

常用 React 属性类型

//通用的,相对比较友好的类型定义
export declare interface AppBetterProps {
  children: React.ReactNode // 一般情况下推荐使用,支持所有类型 Great
  functionChildren: (name: string) => React.ReactNode
  style?: React.CSSProperties // 传递style对象
  onChange?: React.FormEventHandler<HTMLInputElement>
}

//比较一般的类型定义
export declare interface AppProps {
  children1: JSX.Element // 差, 不支持数组
  children2: JSX.Element | JSX.Element[] // 一般, 不支持字符串
  children3: React.ReactChildren // 忽略命名,不是一个合适的类型,工具类类型
  children4: React.ReactChild[] // 很好
  children: React.ReactNode // 最佳,支持所有类型 推荐使用
  functionChildren: (name: string) => React.ReactNode // recommended function as a child render prop type
  style?: React.CSSProperties // 传递style对象
  onChange?: React.FormEventHandler<HTMLInputElement> // 表单事件, 泛型参数是event.target的类型
}

Forms and Events

onChange

change 事件,有两个定义参数类型的方法。

第一种方法使用推断的方法签名(例如:React.FormEvent <HTMLInputElement> :void

type changeFn = (e: React.FormEvent<HTMLInputElement>) => void;

const App: React.FC = () => {
  const [state, setState] = React.useState('');
  const onChange: changeFn = (e) => {
    setState(e.currentTarget.value);
  };
  return (
    <div>
      <input type='text' value={state} onChange={onChange} />
    </div>
  );
};

第二种方法强制使用 @types / react 提供的委托类型,两种方法均可。

const App: React.FC = () => {
  const [state, setState] = React.useState('');

  const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setState(e.currentTarget.value);
  };

  return (
    <div>
      <input type='text' value={state} onChange={onChange} />
    </div>
  );
};

onSubmit

如果不太关心事件的类型,可以直接使用 React.SyntheticEvent,如果目标表单有想要访问的自定义命名输入,可以使用类型扩展

const App: React.FC = () => {
  const onSubmit = (e: React.SyntheticEvent) => {
    e.preventDefault();
    // 类型扩展
    const target = e.target as typeof e.target & {
      password: { value: string };
    }; 
    const password = target.password.value;
  };
  return (
    <form onSubmit={onSubmit}>
      <div>
        <label>
          Password:
          <input type='password' name='password' />
        </label>
      </div>
      <div>
        <input type='submit' value='Log in' />
      </div>
    </form>
  );
};

Operators

常用的操作符,常用于类型判断

  • typeof and instanceof: 用于类型区分
  • keyof: 获取 object 的 key
  • O[K]: 属性查找
  • [K in O]: 映射类型
  • + or - or readonly or ?: 加法、减法、只读和可选修饰符
  • x ? Y : Z: 用于泛型类型、类型别名、函数参数类型的条件类型
  • !: 可空类型的空断言
  • as: 类型断言
  • is: 函数返回类型的类型保护(没用过)

Tips

使用查找类型访问组件属性类型

通过查找类型减少 type 的非必要导出,如果需要提供复杂的 type,应当提取到作为公共 API 导出的文件中。

现在我们有一个 Counter 组件,需要 name 这个必传参数:

// counter.tsx
import * as React from 'react'
export type Props = {
  name: string;
};
const Counter: React.FC<Props> = (props) => {
  return <></>;
};
export default Counter;

在其他引用它的组件中我们有两种方式获取到 Counter 的参数类型

第一种是通过 typeof 操作符(推荐):

// Great
import Counter from './d-tips1'
//获取Counter的类型并添加新的属性age
type PropsNew = React.ComponentProps<typeof Counter> & {
  age: number
}
const App: React.FC<PropsNew> = props => {
  return <Counter {...props} />
}
export default App

第二种是通过原组件进行导出:

import Counter, { Props } from './d-tips1'
type PropsNew = Props & {
  age: number
}
const App: React.FC<PropsNew> = props => {
  return (
    <>
      <Counter {...props} />
    </>
  )
}
export default App

不要在 type 或 interface 中使用函数声明

保持一致性,类型/接口的所有成员都通过相同的语法定义。

–strictFunctionTypes 在比较函数类型时强制执行更严格的类型检查,但第二种声明方式下严格检查不生效。

✅ 尽量使用这种方式
interface ICounter {
  start: (value: number) => string
}

❌ 最好别用这种方式
interface ICounter1 {
  start(value: number): string
}

示例:

interface Animal {}
interface Dog extends Animal {
  wow: () => void
}

//使用这种类型定义检查更严格
interface Comparer<T> {
  compare: (a: T, b: T) => number
}
declare let animalComparer: Comparer<Animal>
declare let dogComparer: Comparer<Dog>
animalComparer = dogComparer // Error	不能将类型“Comparer<Dog>”分配给类型“Comparer<Animal>”。 类型 "Animal" 中缺少属性 "wow",但类型 "Dog" 中需要该属性
dogComparer = animalComparer // Ok

//使用这种类型定义检查较宽松,容易引发未知问题
interface Comparer1<T> {
  compare(a: T, b: T): number
}
declare let animalComparer1: Comparer1<Animal>
declare let dogComparer1: Comparer1<Dog>
animalComparer1 = dogComparer1 // Ok
dogComparer1 = animalComparer1 // Ok

事件处理

我们在进行事件注册时经常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时我们通过 clientXclientY 去获取指针的坐标。

大家可能会想到直接把 event 设置为 any 类型,但是这样就失去了我们对代码进行静态检查的意义。

function handleEvent(event: any) {console.log(event.clientY)
}

试想下当我们注册一个 Touch 事件,然后错误的通过事件处理函数中的 event 对象去获取其 clientY 属性的值,在这里我们已经将 event 设置为 any 类型,导致 TypeScript 在编译时并不会提示我们错误, 当我们通过 event.clientY 访问时就有问题了,因为 Touch 事件的 event 对象并没有 clientY 这个属性。

通过 interface 对 event 对象进行类型声明编写的话又十分浪费时间,幸运的是 React 的声明文件提供了 Event 对象的类型声明。

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> 过渡事件对象
事件处理函数类型

当我们定义事件处理函数时有没有更方便定义其函数类型的方式呢?答案是使用 React 声明文件所提供的 EventHandler 类型别名,通过不同事件的 EventHandler 的类型别名来定义事件处理函数的类型

type EventHandler<E extends React.SyntheticEvent<any>> = {
  bivarianceHack(event: E): void
}['bivarianceHack']
type ReactEventHandler<T = Element> = EventHandler<React.SyntheticEvent<T>>
type ClipboardEventHandler<T = Element> = EventHandler<React.ClipboardEvent<T>>
type DragEventHandler<T = Element> = EventHandler<React.DragEvent<T>>
type FocusEventHandler<T = Element> = EventHandler<React.FocusEvent<T>>
type FormEventHandler<T = Element> = EventHandler<React.FormEvent<T>>
type ChangeEventHandler<T = Element> = EventHandler<React.ChangeEvent<T>>
type KeyboardEventHandler<T = Element> = EventHandler<React.KeyboardEvent<T>>
type MouseEventHandler<T = Element> = EventHandler<React.MouseEvent<T>>
type TouchEventHandler<T = Element> = EventHandler<React.TouchEvent<T>>
type PointerEventHandler<T = Element> = EventHandler<React.PointerEvent<T>>
type UIEventHandler<T = Element> = EventHandler<React.UIEvent<T>>
type WheelEventHandler<T = Element> = EventHandler<React.WheelEvent<T>>
type AnimationEventHandler<T = Element> = EventHandler<React.AnimationEvent<T>>
type TransitionEventHandler<T = Element> = EventHandler<
  React.TransitionEvent<T>
>

用法:

const App = () => {
  const onClick: React.MouseEventHandler<HTMLDivElement> = (e) => {
    alert(e);
  };
  return <div onClick={onClick}>TEST</div>;
};

bivarianceHack 为事件处理函数的类型定义,函数接收一个 event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void

关于为何是用 bivarianceHack 而不是(event: E): void,这与 strictfunctionTypes 选项下的功能兼容性有关。(event: E): void,如果该参数是派生类型,则不能将其传递给参数是基类的函数。

示例(不太理解其实):

class Animal {
  private x: undefined;
}
class Dog extends Animal {
  private d: undefined;
}
type EventHandler<E extends Animal> = (event: E) => void;
// fails under strictFunctionTyes 不能将类型“(o: Dog) => void”分配给类型“EventHandler<Animal>”。参数“o”和“event” 的类型不兼容。类型 "Animal" 中缺少属性 "d",但类型 "Dog" 中需要该属性。
let z: EventHandler<Animal> = (o: Dog) => {}; 
type BivariantEventHandler<E extends Animal> = {
  bivarianceHack(event: E): void;
}['bivarianceHack'];
let y: BivariantEventHandler<Animal> = (o: Dog) => {};

Promise 类型

在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。Promise<T> 是一个泛型类型,T 泛型变量用于确定 then 方法时接收的第一个回调函数的参数类型。

//这里用泛型T主要是可扩展,毕竟result可能是任何类型
type IResponse<T> = {
  message: string;
  result: T;
  success: boolean;
};
async function getResponse(): Promise<IResponse<number[]>> {
  return {
    message: '获取成功',
    result: [1, 2, 3],
    success: true,
  };
}

getResponse().then((response) => {
  console.log(response.result);
});

首先声明 IResponse 的泛型接口用于定义 response 的类型,通过 T 泛型变量来确定 result 的类型。然后声明了一个 异步函数 getResponse 并且将函数返回值的类型定义为 Promise<IResponse<number[]>> 。最后调用 getResponse 方法会返回一个 promise 类型,通过 then 调用,此时 then 方法接收的第一个回调函数的参数 response 的类型为,{ message: string, result: number[], success: boolean} 。

泛型参数的组件

下面这个组件的 name 属性都是指定了传参格式,如果想不指定,而是想通过传入参数的类型去推导实际类型,这就要用到泛型。

const TestB = ({ name, name2 }: { name: string; name2?: string }) => {
  return (
    <div className='test-b'>
      TestB--{name}
      {name2}
    </div>
  );
};

如果需要外部传入参数类型,只需 ->

type Props<T> = {
  name: T;
  name2?: T;
};
const TestC: <T extends React.ReactNode>(
  props: Props<T>
) => React.ReactElement = ({ name, name2 }) => {
  return (
    <div className='test-b'>
      TestB--{name}
      {name2}
    </div>
  );
};

const TestD = () => {
  return (
    <div>
      <TestC<string> name='123' />
    </div>
  );
};

什么时候使用泛型

当你的函数,接口或者类:

  • 需要作用到很多类型的时候,举个 ?

当我们需要一个 id 函数,函数的参数可以是任何值,返回值就是将参数原样返回,并且其只能接受一个参数,在 js 时代我们会很轻易地甩出一行

const id = arg => arg

由于其可以接受任意值,也就是说我们的函数的入参和返回值都应该可以是任意类型,如果不使用泛型,我们只能重复的进行定义

type idBoolean = (arg: boolean) => boolean
type idNumber = (arg: number) => number
type idString = (arg: string) => string

如果使用泛型,我们只需要

function id<T>(arg: T): T {
  return arg
}

// 或
const id1: <T>(arg: T) => T = arg => {
  return arg
}
  • 需要被用到很多地方的时候,比如常用的工具泛型 Partial。(没懂)

功能是将类型的属性变成可选, 注意这是浅 Partial

type Partial<T> = { [P in keyof T]?: T[P] }

如果需要深 Partial 我们可以通过泛型递归来实现

type DeepPartial<T> = T extends Function
  ? T
  : T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T
type PartialedWindow = DeepPartial<Window>

参考资料

[1]

2ality’s guide: http://2ality.com/2018/04/type-notation-typescript.html

[2]

chibicode’s tutorial: https://ts.chibicode.com/todo/

[3]

TS 部分: https://reactjs.org/docs/static-type-checking.html#typescript

[4]

React 部分: http://www.typescriptlang.org/play/index.html?jsx=2&esModuleInterop=true&e=181#example/typescript-with-react

[5]

被证明: https://www.reddit.com/r/reactjs/comments/iyehol/import_react_from_react_will_go_away_in_distant/

[6]

一些 issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33006

[7]

问题: https://github.com/babel/babel/issues/9800

[8]

参考链接: https://twitter.com/hswolff/status/1133759319571345408

[9]

TypeScript3.0+: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html

[10]

存在一些边界 case 仍然存在问题: https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/issues/61

转载请注明出处或者链接地址:https://www.qianduange.cn//article/10703.html
标签
评论
发布的文章

js-jquery页面跳转集合

2024-06-08 22:06:13

echarts柱状图数据过多

2024-06-08 22:06:31

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!