WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

TypeScript types #120

Open WangShuXian6 opened 3 years ago

WangShuXian6 commented 3 years ago

TypeScript 类型

https://www.typescriptlang.org/zh/play TypeScript: 演练场 - 一个用于 TypeScript 和 JavaScript 的在线编辑器

WangShuXian6 commented 3 years ago

React TypeScript types

https://react-typescript-cheatsheet.netlify.app/

如何引入 React

推荐

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


import * as React from 'react'

import * as ReactDOM from 'react-dom'


#### 另外一种引用方式:
>需要添加额外的配置:`"allowSyntheticDefaultImports": true`
```tsx
import React from 'react'

import ReactDOM from 'react-dom'

函数式组件的声明方式

React.FunctionComponent 简写形式:React.FC

不推荐 它提供了一个隐式定义children(见下文)——但是隐式children类型存在一些问题(例如DefineTyped#33006),无论如何,对组件属性进行明确定义可能会更好。


type AppProps = {
message: string
}

const App: React.FC = ({ message, children }) => (

{message} {children}

)


##### `React.FC` 声明函数组件和普通声明以及 `PropsWithChildren` 的区别
>`React.FC` 显式地定义了返回类型,其他方式是隐式推导的

>`React.FC` 对静态属性:`displayName`、`propTypes`、`defaultProps` 提供了类型检查和自动补全

>`React.FC` 为 `children` 提供了隐式的类型`(ReactElement | null)`,但是目前,提供的类型存在一些 issue

#### PropsWithChildren
>省去频繁定义 children 的类型,自动设置 children 类型为 ReactNode:
```tsx
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) => (

{message} {children}

)


### Hooks

#### `useState<T>`
>状态初始值为空时(null),需要显示地声明类型:
```tsx
type User = {
  name: string
  age: number
}
const [user, setUser] = React.useState<User | null>(null)

useRef<T>

React.useRef只能是null或元素对象【element object】

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

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

useRef + forwardRef

forwardRef 可能在在 react17中废弃 ,不推荐使用


import { useState, useCallback, useRef, forwardRef, useImperativeHandle } from "react"

//您需要在别处提取 ref 类型: interface RefObject { SayHi: () => void }

// 然后在两个地方都引用它

const Child = forwardRef((props: { name: string }, ref: Ref) => { const { name } = props; useImperativeHandle(ref, () => ({ SayHi })); function SayHi() { console.log("Hello " + name); } return

{name}
; });

const Parent = () => { const ref = useRef(null); const onButtonClick = () => { if (ref.current) { ref.current.SayHi(); } }; return (

);

}


##### 区别
>第一种方式的 ref1.current 是只读的(read-only),并且可以传递给内置的 ref 属性,绑定 DOM 元素 ;

>第二种方式的 ref2.current 是可变的(类似于声明类的成员变量)

>这两种方式在使用时,都需要对类型进行检查:
```tsx
const onButtonClick = () => {
  ref1.current?.focus()
  ref2.current?.focus()
}

useEffect

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

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

useMemo<T> useCallback<T>

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

useMemo 的泛型指定了返回值类型 useCallback 的泛型指定了参数类型

// 也可以显式的指定返回值类型,返回值不一致会报错
const result = React.useMemo<string>(() => 2, [])
// 类型“() => number”的参数不能赋给类型“() => string”的参数。
const handleChange = React.useCallback<
React.ChangeEventHandler<HTMLInputElement>
(evt => {
console.log(evt.target.value)
}, [])

useReducer

const initialState = { count: 0 };

type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string };

function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - Number(action.payload) };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}

自定义 Hooks

const 断言

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

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
}

直接定义返回类型

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]
}

defaultProps

不推荐使用 defaultProps , 相关讨论 https://twitter.com/hswolff/status/1133759319571345408

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

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

Types or Interfaces

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


class Point {
x: number = 2
y: number = 3
}
interface IShape {
area(): number
}
type Perimeter = {
perimeter(): number
}
type RectangleShape = (IShape | Perimeter) & Point

class Rectangle implements RectangleShape { // 类只能实现具有静态已知成员的对象类型或对象类型的交集。 x = 2 y = 3 area() { return this.x + this.y } } interface ShapeOrPerimeter extends RectangleShape {} // 接口只能扩展使用静态已知成员的对象类型或对象类型的交集


#### 使用 Type 还是 Interface?
>有几种常用规则:

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

#### type 和 interface 最大的区别

>type 类型不能二次编辑
>而 interface 可以随时扩展
```tsx
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 'library' // 但是未导出props type
type ButtonProps = React.ComponentProps<typeof Button> // 获取props
type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick
const AlertButton: React.FC<AlertButtonProps> = props => (
  <Button onClick={() => alert('hello')} {...props} />
)
// 获取返回值类型
function foo() {
  return { baz: 1 }
}
type FooReturn = ReturnType<typeof foo> // { baz: number }

常用 Props ts 类型

对象类型

type ObjectA=Record<string, unknown>

基础属性类型

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'
  /** 任意需要使用其属性的对象(不推荐使用,但是作为占位很有用) */

  /** 列出对象全部数量的属性 (推荐使用) */
  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 :void) input 元素的表单事件


import * as React from 'react'

type changeFn = (e: React.FormEvent) => void const App: React.FC = () => { const [state, setState] = React.useState('') const onChange: changeFn = e => { setState(e.currentTarget.value) } return (

) }


>第二种方法强制使用 @types / react 提供的委托类型,两种方法均可。
```tsx
import * as React from '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,如果目标表单有想要访问的自定义命名输入,可以使用类型扩展

import * as React from 'react'

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' type PropsNew = React.ComponentProps & { age: number } const App: React.FC = props => { return <Counter {...props} /> } export default App


>第二种是通过在原组件进行导出
```tsx
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 中使用函数声明

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

✅

interface ICounter {
  start: (value: number) => string
}

❌

interface ICounter1 {
  start(value: number): string
}

事件处理

React 的声明文件提供了 Event 对象的类型声明。 浏览器中的 JavaScript 已经包含一系列 HTML 事件,JavaScript 可以对这些事件进行侦听和响应。 例如onclick,或onchange。 但是,这些事件的实现和行为因浏览器而异。 React 使用合成事件解决了这个问题,合成事件是原生事件的包装器,在所有浏览器中都一样。 本机属性以小写 ( onclick)编写,而合成事件使用驼峰式 ( onClick)。

当我们在 React 中添加事件处理程序时,我们不能使用 TypeScript 包含在DOM 库中的类型,因为 React 为这些事件添加了一个包装器。 相反,我们需要使用 React 提供的类型定义。

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> 过渡事件对象

import React, { ChangeEventHandler, ChangeEvent } from 'react';

// This
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {}

// is the same as this
const handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {}

Promise 类型

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

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>(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>
  )
}
泛型
function id<T>(arg: T): T {
  return arg
}

// 或
const id1: <T>(arg: T) => T = arg => {
  return arg
}
WangShuXian6 commented 2 years ago

按用例分类的有用模式 Useful Patterns by Use Case for React

Wrapping/Mirroring 包装/镜像

Wrapping/Mirroring a HTML Element 包装/镜像 HTML 元素

用例:自定义一个<Button>,它拥有<button>的所有正常属性并添加一些额外的属性。

策略:继承 React.ComponentPropsWithoutRef<'button'>


// usage
function App() {
// Type '"foo"' is not assignable to type '"button" | "submit" | "reset" | undefined'.(2322)
// return <Button type="foo"> sldkj </Button>

// no error return ; }

// implementation export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> { specialProp?: string; } export function Button(props: ButtonProps) { const { specialProp, ...rest } = props; // do something with specialProp return <button {...rest} />; }

WangShuXian6 commented 2 years ago

TypeScript Type Example

获取对象key的值

function getPropValue<T extends object, Key extends keyof T>(
  obj: T,
  key: Key
): T[Key] {
  return obj[key];
}
WangShuXian6 commented 2 years ago

react DragEvent typescript type

onDrag

//Interface
interface DragEvent<T = Element> extends MouseEvent<T, NativeDragEvent> {
    dataTransfer: DataTransfer;
}
//Full example
import React, { DragEvent } from 'react';
const ButtonComponent = () => {
  const handleDragEvent = (e: DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    // Do something
  };
  return <div onDrag={handleDragEvent}>Drag me!</div>;
};
export default ButtonComponent;

onDragCapture


//Interface
interface DragEvent<T = Element> extends MouseEvent<T, NativeDragEvent> {
    dataTransfer: DataTransfer;
}
//Full example
import React, { DragEvent } from 'react';
const ButtonComponent = () => {
  const handleDragEvent = (e: DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    // Do something
  };
  return <div onDragCapture={handleDragEvent}>Drag me!</div>;
};
export default ButtonComponent;

WangShuXian6 commented 3 months ago

hooks 类型

findMinAndMaxRounded.ts

export interface HasValue {
  value?: number;
}

export interface MinMax {
  min: number;
  max: number;
}

export function findMinAndMaxRounded<T extends HasValue>(
  values: T[],
  roundTo: number = 5,
  defaultMin: number = Number.MIN_SAFE_INTEGER,
  defaultMax: number = Number.MAX_SAFE_INTEGER
): MinMax {
  if (values.length === 0) {
    throw new Error('Array must not be empty');
  }

  let minValue = defaultMin;
  let maxValue = defaultMax;

  for (const item of values) {
    if (item.value !== undefined) {
      if (item.value < minValue || minValue === Number.MIN_SAFE_INTEGER) {
        minValue = item.value;
      }
      if (item.value > maxValue || maxValue === Number.MAX_SAFE_INTEGER) {
        maxValue = item.value;
      }
    }
  }

  // 如果最终minValue和maxValue仍然为默认值,则说明没有有效的value,可以设置为undefined或进行相应的错误处理
  if (minValue === Number.MIN_SAFE_INTEGER) {
    minValue = 0; // 这里可以根据具体需要设置或抛出异常
  }
  if (maxValue === Number.MAX_SAFE_INTEGER) {
    maxValue = 0; // 这里可以根据具体需要设置或抛出异常
  }

  const roundedMin = Math.floor(minValue / roundTo) * roundTo;
  const roundedMax = Math.ceil(maxValue / roundTo) * roundTo;

  return {
    min: roundedMin,
    max: roundedMax
  };
}

// 示例用法
const items: { value?: number; otherProp: string }[] = [
  { value: 2, otherProp: 'a' },
  { value: 98, otherProp: 'b' },
  { value: 55, otherProp: 'c' },
  { value: 22, otherProp: 'd' },
  { otherProp: 'e' } // 无value的对象
];

const result = findMinAndMaxRounded(items, 5, 0, 100);
console.log(result); // 输出: { min: 0, max: 100 }

useYRange.ts

import { useMemo } from 'react'
import { findMinAndMaxRounded, MinMax, HasValue } from '@/pages/healthRecords/utils'

interface Props<T extends HasValue> {
  list: T[]
  roundTo: number
  defaultYRange: MinMax
}

export function useYRange<T extends HasValue>({ list, roundTo, defaultYRange }: Props<T>): MinMax {
  const { min, max } = useMemo(() => {
    if(list.length === 0) return defaultYRange
    return findMinAndMaxRounded<T>(list, roundTo, defaultYRange.min, defaultYRange.max)
  }, [list, roundTo, defaultYRange.min, defaultYRange.max]) // 确保所有相关依赖项都被包含

  return { min, max }
}

使用


export type BloodGlucoseData = {
date: number
value?: number
type: BloodGlucoseLevel

status?: BloodGlucose.BloodGlucoseStatus dataSource?: DataSource }

export type BloodGlucoseDataWithDay = BloodGlucoseData & { day: string displayType: BloodGlucose.BloodGlucoseDisplayLevel

const roundTo = 1 const defaultYRange = { min: 0, max: 14 }

const { min, max } = useYRange({ list, roundTo, defaultYRange })