Typescript夯实基础——react

2,539 阅读6分钟

typescript基础系列

  1. Typescript夯实基础——打通任通二脉

  2. Typescript夯实基础——react

本文参考

  1. 参考
  2. React组件设计实践总结01 - 类型检查

1.配置项目

• tsconfig.json • ESLint / Prettier • VS Code 扩展和配置

VSCode 扩展和设置

我们添加了 ESLint 和 Prettier ,下一步就是在保存时自动修复/美化我们的代码。 首先,安装 VSCode 的 ESLint extension 和 Prettier extension 。这将使 ESLint 与您的编辑器无缝集成。 接下来,通过将以下内容添加到您的中来更新工作区设置 .vscode/settings.json : { "editor.formatOnSave": true } 保存时, VS Code 会发挥它的魔力并修复您的代码。很棒!

2.组件

因现在流行hooks,所以 react组件以函数组件为主。

import React from 'react'
// 函数声明式写法
function Heading(): React.ReactNode {
  return <h1>My Website Heading</h1>
}
// 函数扩展式写法
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>

React.FC 不使用 React.FC-TypeScript + React: Why I don't use React.FC

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

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

FC是FunctionComponent的简写, 这个类型定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)

3.props

可以使用 interface 或 type 来定义 Props 。让我们看一个例子:

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 社区提出的准则:

  1. 在编写库或第三方环境类型定义时,始终将 interface 用于公共 API 的定义。
  2. 考虑为你的 React 组件的 State 和 Props 使用 type ,因为它更受约束。”

通常,在 React 和 TypeScript 项目中编写 Props 时,请记住以下几点:

  1. 始终使用 TSDoc 标记为你的 Props 添加描述性注释 /** comment */。
  2. 无论你为组件 Props 使用 type 还是 interfaces ,都应始终使用它们。
  3. 如果 props 是可选的,请适当处理或使用默认值。

interface vs type

参考

  1. interface 和 type interface 和 type 都可以用来定义一些复杂的类型结构,最很多情况下是通用的,二者的区别在于:
  2. interface创建了一种新的类型,而 type 仅仅是别名,是一种引用;
  3. 如果 type 使用了 union operator (|) 操作符,则不能将 type implements 到 class 上;
  4. 如果 type 使用了 union(|) 操作符 ,则不能被用以 extends interface 5.type 不能像 interface 那样合并,其在作用域内唯一; [1]

默认 props 声明

实际上截止目前对于上面的使用FC类型声明的函数组件并不能完美支持 defaultProps:

import React, { FC } from 'react';
export interface HelloProps {
  name: string;
}
export const Hello: FC<HelloProps> = ({ name }) => <div>Hello {name}!</div>;
Hello.defaultProps = { name: 'TJ' };
// ❌! missing name
<Hello />;
const TestFunction: FunctionComponent<Props> = ({ foo = "bar" }) => <div>{foo}</div>

如果一定要用defaultProps,则只能按照下面这样玩

import React, { PropsWithChildren } from 'react';
export interface HelloProps {
  name: string;
}
// 直接使用函数参数声明
// PropsWithChildren只是扩展了children, 完全可以自己声明
// type PropsWithChildren<P> = P & {
//    children?: ReactNode;
// }
const Hello = ({ name }: PropsWithChildren<HelloProps>) => <div>Hello {name}!</div>;
Hello.defaultProps = { name: 'TJ' };
// ✅ ok!
<Hello />;

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

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

4.Hooks

使用 Hook 时, TypeScript 类型推断工作得很好。这意味着你没有什么好担心的。举个例子:

// `value` is inferred as a string
// `setValue` is inferred as (newValue: string) => void
const [value, setValue] = useState('')

在极少数情况下,你需要使用一个空值初始化 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);

5.处理表单事件 最常见的情况之一是 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"/>
}
  1. ChangeEvent 类型实现源码 node_modules/@types/react/index.d.ts
  2. EventTarget 类型实现源码 node_modules/typescript/lib/lib.dom.d.ts 。

6.Promise 类型

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

interface 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)
  })
  1. 我们首先声明 IResponse 的泛型接口用于定义 response 的类型,通过 T 泛型变量来确定 result 的类型。

  2. 然后声明了一个 异步函数 getResponse 并且将函数返回值的类型定义为 Promise<IResponse<number[]>> 。

  3. 最后调用 getResponse 方法会返回一个 promise 类型,通过 then 调用,此时 then 方法接收的第一个回调函数的参数 response 的类型为,{ message: string, result: number[], success: boolean} 。 Promise 实现源码 node_modules/typescript/lib/lib.es5.d.ts。

7. 第三方库

无论是用于诸如 Apollo 之类的 GraphQL 客户端还是用于诸如 React Testing Library 之类的测试,我们经常会在 React 和 TypeScript 项目中使用第三方库。发生这种情况时,你要做的第一件事就是查看这个库是否有一个带有 TypeScript 类型定义 @types 包。你可以通过运行:

#yarn
yarn add @types/<package-name>
#npm
npm install @types/<package-name>

• 第三方的库,如何得到类型支持 我们很难保证,第三方的库都原生支持 TS 类型,在你使用过一段时间 TS 后,你肯定安装过类似 @types/xxx 的类型库,安装类似这样的库,实际上就安装了某个库的描述文件,对于这些第三方库的类型的定义,都存储在DefinitelyTyped 这个仓库中,常用的第三方库在这里面都有定义了。在 TypeSearch 中可以搜索第三方库的类型定义包。

更深理解

当然也许你并不会满足于会用 TS,你还想知道 TS 的工作原理是什么。这时候推荐阅读下面两篇内容:

  1. TypeScript Compiler Internals · TypeScript Deep Dive ,TS 编译的核心还是 AST,这篇文章讲解了 TS 编译的五个阶段( Scanner /Parser / Binder /Checker /Emitter )分别是怎么工作的;
  2. Learn how to contribute to the TypeScript compiler on GitHub through a real-world example,则是另外一篇比较好的了解 TS 运行原理的资料。

关于 TS 的原理,还没顾上看,后续大家可以一起研究研究