React的ts类型检查与注意点

大纲

  • React的ts类型检查与注意点
    • 配置
      • *jsx项的选项
    • 常用检查
      • 函数组件的类型检查
        • 使用FC(Function Component)类型来声明函数组件
        • 定义 Props 类型
        • Children
        • 默认 props 声明
        • Dispatch接口
        • 泛型函数组件
        • 子组件声明
        • Forwarding Refs
      • 类组件的类型检查
        • 继承 `Component

React的ts类型检查与注意点

用TypeScript写React的时候,我们可以通过@types/...使用一些类型检查,本文记录这些检查方式的注意点。

配置

首先进行编译配置。配置很简单,先安装@types/react包(npm i -D @types/react),然后在tsconfig.json的编译配置项compilerOptions中配置"jsx"项为"react"即可。

*jsx项的选项

  • "preserve":模式下对代码的编译会保留 jsx 格式,并输出一份 .jsx 后缀的文件;
  • "react":模式下对代码的编译是直接转换成 React.createElement,并输出一份 .js 后缀的文件;
  • "react-native":模式下对代码的编译会保留 jsx 格式,并输出一份 .js 后缀的文件。
Mode Input Output Output File Extension
preserve
.jsx
react
React.createElement("div") .js
react-native
.js

常用检查

函数组件的类型检查

使用FC(Function Component)类型来声明函数组件

FC定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)。如

import React, {
      FC } from 'react';

interface DemoComponentProps {
     
  className?: string;
  style?: React.CSSProperties;
}

const DemoComponent: FC<DemoComponentProps> = props => {
     
  return <div>hello react</div>;
};

也可以直接使用普通函数来进行组件声明,这种形式更加灵活:

interface DemoComponentProps {
     
  className?: string;
  style?: React.CSSProperties;
  // 手动声明children
  children?: React.ReactNode;
}

function DemoComponent(props: DemoComponentProps) {
     
  return <div>hello react</div>;
}

与之类似的还有SFC,但现在已经不再建议使用了,具体原因可见下文@types/react源码分析。

定义 Props 类型

通常使用 ComponentName|Props 的格式命名 Props 类型。如:

import React from 'react'

interface DemoComponentProps {
     
  title: string;
  content: string;
}

function DemoComponent (props: DemoComponentProps) {
     
  const {
     
    title,
    content,
  } = props;
  return (<>
    <p>title: {
     title}</p>
    <p>content: {
     content}</p>
  </>)
}

Children

我们通过设置 React.ReactNode 来设置children。如

import React from 'react'

type CardProps = {
     
  title: string;
  children: React.ReactNode;
};

export function Card({
      title, children }: CardProps) {
     
  return (
    <section className="cards">
      <h2>{
     title}</h2>
      {
     children}
    </section>
  );
}

默认 props 声明

TypeScript 3.0 开始已经支持了 defaultProps ,这也意味着我们可以很方便的定义默认值。如:

import React, {
      PropsWithChildren } from 'react';

export interface HelloProps {
     
  name: string;
}

/**
 * 直接使用函数参数声明
 * PropsWithChildren只是扩展了children, 完全可以自己声明
 * type PropsWithChildren

= P & { * children?: ReactNode; * } */ const Hello = ({ name }: PropsWithChildren<HelloProps>) => <div>Hello { name}!</div>; Hello.defaultProps = { name: 'Wayne' };

这种方式很简洁, 只不过 defaultProps 的类型和组件本身的 props 没有关联性, 这会使得 defaultProps 无法得到类型约束, 所以必要时进一步显式声明 defaultProps 的类型:

Hello.defaultProps = {
      name: 'Wayne' } as Partial<HelloProps>;

Dispatch接口

Dispatch 泛型接口,用于定义dispatch的类型,常用于useReducer生成的dispatch中。

/** 
 * 创建一个异步action的函数,返回一个包含异步action对象
 */
const asyncAction = (dispatch: Dispatch<any>) => {
     
    return {
     
        asyncAddaction() {
     // 一个异步的添加action
            console.log('执行addActions之前: ' + Date.now());
            setTimeout(() => {
     
                console.log('执行addActions : ' + Date.now());
                dispatch(addActions());
            }, 1000);
        }
    }
}

泛型函数组件

泛型函数组件在列表型或容器型的组件中比较常用, 直接使用FC无法满足需求:

import React from 'react';

export interface ListProps<T> {
     
  list: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
}

export function List<T>(props: ListProps<T>) {
     
  return (
  <section>
	{
     
		props.list.map(props.renderItem)
	}
  </section>
  );
}

function TestList() {
     
  return (
    <List
      list={
     [1, 2, 3]}
      renderItem={
     i => {
     
        /* TypeScript推断i为number类型 */
		    return (<p>{
     i}</p>)
      }}
    />
  );
}

子组件声明

使用Parent.Child形式的 JSX 可以让节点父子关系更加直观, 它类似于一种命名空间的机制,可以避免命名冲突,ant design中就大量使用了这类形式。相比ParentChild这种命名方式, Parent.Child更为优雅些。如:

import React, {
      PropsWithChildren } from 'react';

export interface LayoutProps {
     }
export interface LayoutHeaderProps {
     } // 采用ParentChildProps形式命名
export interface LayoutFooterProps {
     }

export function Layout(props: PropsWithChildren<LayoutProps>) {
     
  return <div className="layout">{
     props.children}</div>;
}

// 作为父组件的属性
Layout.Header = (props: PropsWithChildren<LayoutHeaderProps>) => {
     
  return <div className="header">{
     props.children}</div>;
};

Layout.Footer = (props: PropsWithChildren<LayoutFooterProps>) => {
     
  return <div className="footer">{
     props.children}</div>;
};

function TestLayout () {
     
  return (<Layout>
    <Layout.Header>header</Layout.Header>
    <Layout.Footer>footer</Layout.Footer>
  </Layout>)
}

Forwarding Refs

React.forwardRef 在 16.3 新增, 可以用于转发 ref,适用于 HOC 和函数组件。其暴露方法可以使用 ComponentName|Methods的命名规则

/* MyModal.tsx */
import React, {
      useState, useImperativeHandle, FC, useRef, useCallback } from 'react';

export interface MyModalProps {
     
  title?: React.ReactNode;
  onOk?: () => void;
  onCancel?: () => void;
}

export interface MyModalMethods {
     
  show(): void;
}

export const MyModal = React.forwardRef<MyModalMethods, MyModalProps>((props, ref) => {
     
  const [visible, setVisible] = useState();

  // 初始化ref暴露的方法
  useImperativeHandle(ref, () => ({
     
    show: () => setVisible(true),
  }));

  return <Modal visible={
     visible}>...</Modal>;
});

/* Test.tsx */
const Test: FC<{
     }> = props => {
     
  // 引用
  const modal = useRef<MyModalMethods | null>(null);
  const confirm = useCallback(() => {
     
    if (modal.current) {
     
      modal.current.show();
    }
  }, []);

  const handleOk = useCallback(() => {
     }, []);

  return (
    <div>
      <button onClick={
     confirm}>show</button>
      <MyModal ref={
     modal} onOk={
     handleOk} />
    </div>
  );
};

类组件的类型检查

继承 ComponentPureComponent 泛型类,接收两个参数:

  • P:props的类型定义
  • S:state的类型定义
import React from 'react'

export interface CounterProps {
     	// 同样是{ComponentName}Props形式命名
  defaultCount: number; // 可选props, 不需要?修饰
}

/**
 * 组件状态
 */
interface State {
     
  count: number;
}

/**
 * 继承React.Component, 并声明Props和State类型
 */
export class Counter extends React.Component<CounterProps, State> {
     
  /**
   * 默认参数
   */
  public static defaultProps = {
     
    defaultCount: 0,
  };

  /**
   * 初始化State
   */
  public state = {
     
    count: this.props.defaultCount,
  };

  /**
   * 声明周期方法
   */
  public componentDidMount() {
     }
  
  public componentWillUnmount() {
     }

  public componentDidCatch() {
     }

  public componentDidUpdate(prevProps: CounterProps, prevState: State) {
     }

  /**
   * 渲染函数
   */
  public render() {
     
    return (
      <div>
        {
     this.state.count}
        <button onClick={
     this.increment}>Increment</button>
        <button onClick={
     this.decrement}>Decrement</button>
      </div>
    );
  }

  private increment = () => {
     
    this.setState(({
      count }) => ({
      count: count + 1 }));
  };

  private decrement = () => {
     
    this.setState(({
      count }) => ({
      count: count - 1 }));
  };
}

使用static defaultProps定义默认 props

在 defaultProps 中定义的 props 可以不需要?可选操作符修饰,demo如上一个。

子组件声明

类组件可以使用静态属性形式声明子组件,如:

import React from 'react'
import Header from './Header'
import Footer from './Footer'

export class Layout extends React.Component<LayoutProps> {
     
  public static Header = Header;
  public static Footer = Footer;

  public render() {
     
    return <div className="layout">{
     this.props.children}</div>;
  }
}

泛型组件

与函数组件的泛型组件类似,如:

import React from 'react'

export class List<T> extends React.Component<ListProps<T>> {
     
  public render() {
     }
}

其他函数组件的类型定义

HTML元素的扩展属性

你可以通过 JSX.IntrinsicElements 集合确保你能够设置一个元素的所有HTML属性。如:

import React from 'react'

type ButtonProps = JSX.IntrinsicElements["button"];

function Button({
      ...allProps }: ButtonProps) {
     
  return <button {
     ...allProps} />;
}

预设属性:

import React from 'react'

type ButtonProps =
  Omit<JSX.IntrinsicElements["button"], "type">;

function Button({
      ...allProps }: ButtonProps) {
     
  return <button type="button" {
     ...allProps} />;
}

const z = <Button type="button">Hi</Button>; 

Context

Context 提供了一种跨组件间状态共享机制。通常我们使用 Name|ContextValue 的命名规范声明Context的类型。

import React, {
      FC, useContext } from 'react';

export interface Theme {
     
  primary: string;
  secondary: string;
}

export interface ThemeContextValue {
     
  theme: Theme;
  onThemeChange: (theme: Theme) => void;
}

/**
 * 创建Context, 并设置默认值, 以 Name|Context 的格式命名
 */
export const ThemeContext = React.createContext<ThemeContextValue>({
     
  theme: {
     
    primary: 'red',
    secondary: 'blue',
  },
  onThemeChange: noop,
});

/**
 * Provider, 以{Name}Provider命名
 */
export const ThemeProvider: FC<{
      theme: Theme; onThemeChange: (theme: Theme) => void }> = props => {
     
  return (
    <ThemeContext.Provider value={
     {
      theme: props.theme, onThemeChange: props.onThemeChange }}>
      {
     props.children}
    </ThemeContext.Provider>
  );
};

/**
 * 暴露hooks, 以use{Name}命名
 */
export function useTheme() {
     
  return useContext(ThemeContext);
}

*高阶组件的类型检查

老实说不建议使用HOC。高阶组件笨重且难以理解,容易造成嵌套地狱(wrapper),对 Typescript 类型化也不友好。不过还是举个栗子:

import React, {
      FC } from 'react';

export interface ThemeProps {
     
  primary: string;
  secondary: string;
}

/**
 * 给指定组件注入'主题'
 */
export function withTheme<P>(Component: React.ComponentType<P & ThemeProps>) {
     
  /**
   * WithTheme 自己暴露的Props
   */
  interface OwnProps {
     }

  /**
   * 高阶组件的props, 忽略ThemeProps, 外部不需要传递这些属性
   */
  type WithThemeProps = P & OwnProps;

  /**
   * 高阶组件
   */
  const WithTheme = (props: WithThemeProps) => {
     
    // 假设theme从context中获取
    const fakeTheme: ThemeProps = {
     
      primary: 'red',
      secondary: 'blue',
    };
    return <Component {
     ...fakeTheme} {
     ...props} />;
  };

  WithTheme.displayName = `withTheme${
       Component.displayName}`;

  return WithTheme;
}

// Test
const Foo: FC<{
      a: number } & ThemeProps> = props => <div style={
     {
      color: props.primary }} />;
const FooWithTheme = withTheme(Foo);
() => {
     
  <FooWithTheme a={
     1} />;
};

/**
 * 抽取出通用的高阶组件类型
 */
type HOC<InjectedProps, OwnProps = {
     }> = <P>(
  Component: React.ComponentType<P & InjectedProps>,
) => React.ComponentType<P & OwnProps>;

/**
 * 声明注入的Props
 */
export interface ThemeProps {
     
  primary: string;
  secondary: string;
}

export const withTheme: HOC<ThemeProps> = Component => props => {
     
  // 假设theme从context中获取
  const fakeTheme: ThemeProps = {
     
    primary: 'red',
    secondary: 'blue',
  };
  return <Component {
     ...fakeTheme} {
     ...props} />;
};

*Render Props

React 的 props(包括 children)并没有限定类型,它可以是一个函数。于是就有了 render props, 这是和高阶组件一样常见的模式:

import React from 'react';

export interface ThemeConsumerProps {
     
  children: (theme: Theme) => React.ReactNode;
}

export const ThemeConsumer = (props: ThemeConsumerProps) => {
     
  const fakeTheme = {
      primary: 'red', secondary: 'blue' };
  return props.children(fakeTheme);
};

// Test
<ThemeConsumer>
  {
     ({
      primary }) => {
     
    return <div style={
     {
      color: primary }} />;
  }}
</ThemeConsumer>;

事件的类型定义

使用handleEvent命名事件处理器.

如果存在多个相同事件处理器, 则按照 handle|Type|Event 的格式命名, 例如: handleNameChange

import React from 'react';

export const EventDemo: FC<{
     }> = props => {
     
  const handleClick = useCallback<React.MouseEventHandler>(evt => {
     
    evt.preventDefault();
    // ...
  }, []);

  return <button onClick={
     handleClick} />;
};

事件Events

常用 Event 事件对象类型:

  • ClipboardEvent 剪贴板事件对象

  • DragEvent 拖拽事件对象

  • ChangeEvent Change 事件对象

  • KeyboardEvent 键盘事件对象

  • MouseEvent 鼠标事件对象

  • TouchEvent 触摸事件对象

  • WheelEvent 滚轮事件对象

  • AnimationEvent 动画事件对象

  • TransitionEvent 过渡事件对象

  • FormEvent:一个react的form表单event的类型

demos:

<form 
	onSubmit={
     (e:FormEvent)=>{
     
	    e.preventDefault();//取消默认事件
}}>
<input 
	type="text" 
	value={
     count} 
	onChange={
     (e: ChangeEvent<HTMLInputElement>) => {
     
	   setCount(e.currentTarget.value);// HTMLInputElement表示这个一个html的input节点
	}} />

内置事件处理器的类型

@types/react内置了以下事件处理器的类型:

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

可以简洁地声明事件处理器类型:

import {
      ChangeEventHandler } from 'react';
export const EventDemo: FC<{
     }> = props => {
     
  /**
   * 可以限定具体Target的类型
   */
  const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(evt => {
     
    console.log(evt.target.value);
  }, []);

  return <input onChange={
     handleChange} />;
};

自定义组件暴露事件处理器类型

和原生 html 元素一样, 自定义组件应该暴露自己的事件处理器类型, 尤其是较为复杂的事件处理器, 这样可以避免开发者手动为每个事件处理器的参数声明类型
自定义事件处理器类型以 ComponentName|Event|Handler 的格式命名。 为了和原生事件处理器类型区分, 不使用EventHandler形式的后缀

import React, {
      FC, useState } from 'react';

export interface UploadValue {
     
  url: string;
  name: string;
  size: number;
}

/**
 * 暴露事件处理器类型
 */
export type UploadChangeHandler = (value?: UploadValue, file?: File) => void;

export interface UploadProps {
     
  value?: UploadValue;
  onChange?: UploadChangeHandler;
}

export const Upload: FC<UploadProps> = props => {
     
  return <div>...</div>;
};

其他类型检查

获取原生元素 props 定义

有些场景我们希望原生元素扩展一下一些 props. 所有原生元素 props 都继承了React.HTMLAttributes, 某些特殊元素也会扩展了自己的属性, 例如InputHTMLAttributes. 具体可以参考React.createElement方法的实现

import React, {
      FC } from 'react';

export function fixClass<
  T extends Element = HTMLDivElement,
  Attribute extends React.HTMLAttributes<T> = React.HTMLAttributes<T>
>(cls: string, type: keyof React.ReactHTML = 'div') {
     
  const FixedClassName: FC<Attribute> = props => {
     
    return React.createElement(type, {
      ...props, className: `${
       cls} ${
       props.className}` });
  };

  return FixedClassName;
}

/**
 * Test
 */
const Container = fixClass('card');
const Header = fixClass('card__header', 'header');
const Body = fixClass('card__body', 'main');
const Footer = fixClass('card__body', 'footer');

const Test = () => {
     
  return (
    <Container>
      <Header>header</Header>
      <Body>header</Body>
      <Footer>footer</Footer>
    </Container>
  );
};

styled-components

styled-components 是流行的CSS-in-js库, Typescript 在 2.9 支持泛型标签模板,因此可以简单地对 styled-components 创建的组件进行类型约束

// 依赖于@types/styled-components
import styled from 'styled-components/macro';

const Title = styled.h1<{
      active?: boolean }>`
  color: ${
       props => (props.active ? 'red' : 'gray')};
`;

// 扩展已有组件
const NewHeader = styled(Header)<{
      customColor: string }>`
  color: ${
       props => props.customColor};
`;

为没有提供 Typescript 声明文件的第三方库自定义模块声明

// global.d.ts

// 自定义模块声明
declare module 'awesome-react-component' {
     
  // 依赖其他模块的声明文件
  import * as React from 'react';
  export const Foo: React.FC<{
      a: number; b: string }>;
}

axios的类型定义

import axios, {
      AxiosInstance, AxiosRequestConfig, AxiosResponse,AxiosError } from 'axios'
const server: AxiosInstance = axios.create();
server.interceptors.request.use((config: AxiosRequestConfig) => {
     //请求拦截
    return config;
});
server.interceptors.response.use((res: AxiosResponse) => {
     
    if (res.status === 200) {
     //请求成功后 直接需要的返回数据
        res = res.data;
    }
    return res;
},(err:AxiosError)=>{
     });

Preact的类型检查

首先tsconfig.json配置与React不同:

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxFactory": "h",	// Preact的虚拟DOM解析
    "jsxFragmentFactory": "Fragment"	// Preact的fragment组件解析
    ...
  }
  ...
}

并且比较“坑”的是,需要在每个组件中引入Fragment和h,用以告知ts的解析模式。

import {
      Fragment, h } from 'preact'

@types/react源码分析

@types/react的源码其实就是仔细定义了一些React涉及到的接口,源码的备注也比较完整。源码地址:github @types/react

SFC(Stateless Function Component)和FC

早前一直好奇SFC和FC的区别,在看源码时发现

type SFC<P = {
     }> = FunctionComponent<P>;
type FC<P = {
     }> = FunctionComponent<P>;

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

在目前的@types/react定义中,SFC和FC指向都是FunctionComponent这个接口,也就是说它们是一样的。
主要原因可见DefinitelyTyped pull-30364,简单来说,SFC是被弃用的,但为保证兼容旧业务和提升语义性仍然保留。总的结果就是React.SFC、React.StatelessComponent、React.FC、React.FunctionComponent都是同一个接口

*以前的SFC

type SFC<P = {
     }> = StatelessComponent<P>;
interface StatelessComponent<P = {
     }> {
     
    (props: P & {
      children?: ReactNode }, context?: any): ReactElement<any> | null;
    propTypes?: ValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

发现区别其实就是propTypes项ValidationMapWeakValidationMap的区别(type PropsWithChildren

= P & { children?: ReactNode };,ValidationMap的定义可见@types/prop-types),即旧SFC的props校验更为严格:

// ValidationMap
export type ValidationMap<T> = {
      [K in keyof T]?: Validator<T[K]> };

// WeakValidationMap
type WeakValidationMap<T> = {
     
    [K in keyof T]?: null extends T[K]
        ? Validator<T[K] | null | undefined>
        : undefined extends T[K]
        ? Validator<T[K] | null | undefined>
        : Validator<T[K]>
};

// Validator
export interface Validator<T> {
     
    (props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null;
    [nominalTypeHack]?: T;
}

Events

@types/react花了很大的篇幅进行了事件接口的封装,毕竟DOM是前端中最复杂的模块之一了。如触摸事件的封装:

interface TouchEvent<T = Element> extends SyntheticEvent<T, NativeTouchEvent> {
     
    altKey: boolean;
    changedTouches: TouchList;
    ctrlKey: boolean;
    /**
     * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
     */
    getModifierState(key: string): boolean;
    metaKey: boolean;
    shiftKey: boolean;
    targetTouches: TouchList;
    touches: TouchList;
}

@types/react中的Content接口

    interface Context<T> {
     
        Provider: Provider<T>;
        Consumer: Consumer<T>;
        displayName?: string;
    }
	

可以发现我们需要传递一个类型,从而使得里面的参数类型也是一致。

其他

@types/react有许多设计比较巧妙的地方,可以通过《@types react 值得注意的 TS 技巧》进行引读。


注意点

1.不要直接使用export default导出匿名函数组件

export default (props: {
     }) => {
     
  return <div>hello react</div>;
};

这种方式导出的组件在React Inspector查看时会显示为Unknown。可修改为:

export default Hello (props: {
     }) => {
     
  return <div>hello react</div>;
};

2.放弃PropTypes

有了 Typescript 之后可以安全地约束 Props 和 State, 没有必要引入 React.PropTypes, 而且它的表达能力比较弱

3.关于是否使用FC的争议

关于是否使用FC一直存在争议,如《typescript-react-why-i-dont-use-react-fc》,其中给了5条理由,总得来说就是不用FC会更加灵活和更具拓展性。

以我个人的观点来看,FC会让代码更具语义性,如果能保证项目没有迁移类React技术栈(preact、taro、rax等)的情况下,建议使用。


相关链接

  • https://fettblog.eu/typescript-react-component-patterns/
  • https://www.typescriptlang.org/docs/handbook/jsx.html
  • https://stackoverflow.com/questions/53885993/react-16-7-react-sfc-is-now-deprecated/53886046#53886046
  • https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react
  • https://github.com/DefinitelyTyped/DefinitelyTyped/tree/73176410af2dfd18dd7c4a28e19fb182a42a239e
  • https://fettblog.eu/typescript-react-why-i-dont-use-react-fc/
  • https://github.com/dt-fe/weekly/blob/v2/147.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40types%20react%20%E5%80%BC%E5%BE%97%E6%B3%A8%E6%84%8F%E7%9A%84%20TS%20%E6%8A%80%E5%B7%A7%E3%80%8B.md
  • https://fettblog.eu/typescript-vite-preact/

你可能感兴趣的:(前端,React,reactjs,typescript)