初始化项目
这里使用create-react-app命令来创建项目:
$ sudo npm i create-react-app -g
$ create-react-app my-react-app --template typescript
回车,就可以初始化一个react + TypeScript 本地项目。目录结构如下:
node_modules直接装好了,直接启动
cd react-ts
npm run start
项目正常启动。
理解声明文件
react-app-env.d.ts
/// <reference types="react-scripts" />
这个文件是 声明文件,用来声明全局变量使用的。具体参考文档:http://ts.xcatliu.com/
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明的变量一般都是以declare 开头,也可以是/// <reference />
三斜线指令。声明文件中声明的变量,全局可以访问。
- 三斜线指令的使用场景
在全局变量的声明文件中,是不允许出现 import
, export
关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了。
如果把react-app-env.d.ts中的指令注释掉,看下会怎么?
App.tsx文件中会出现这样的提示;
找不到图片的声明文件,页面没有报错,可以正常运行,说明ts的语法错误不应该正常编译。
找到nodule_module文件中react-scripts中的类型声明文件,内容如下:
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
readonly PUBLIC_URL: string;
}
}
declare module '*.bmp' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<
SVGSVGElement
> & { title?: string }>;
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { readonly [key: string]: string };
export default classes;
}
可以看到这个文件主要声明了一些全局模块 。
declare module
扩展模块,也可用于在一个文件中一次性声明多个模块的类型
比如在tsx中引入以。svg结尾的文件, import logo from './logo.svg',就相当于引入了这样的一个模块。可以在任何地方引入,因为声明的模块是全局的。鼠标放上去,就会看到类型提示:
注意,三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。
假如我在我的项目中需要使用第三方工具库lodash。在js,或者jsx文件中,我直接安装引入就可以了,但是在ts或者tsx文件中,不能这样做。
安装好lodash后,在App.tsx文件中引入,就会出现下面的提示:
提示要安装@type/lodash或者包含declare module 'lodash' 的声明文件。
现在声明文件中加上模块声明:
react-app-env.d.ts
declare module 'lodash'
提示改变了。
再通过npm 引入类型包,先把声明文件中的声明语句去掉
tnpm install @types/lodash --save--dev
提示改变了:
这样就可以在项目中使用lodash带有的方法了。
如果react-app-env.d.ts文件中有声明lodash模块的语句,就会被优先使用,忽略node_modules中的声明。
大部分第三方库都带有自己的声明文件,可以通过 @type/module 方式安装。默认会自动识别。
相关的详细内容可以参考:http://ts.xcatliu.com/basics/declaration-files.html#declare-module
tsconfig.json配置文件
tsconfig.json包含了工程里TypeScript特定的选项,是TypeScript的配置文件,项目要想使用TypeScript需要增加这个文件。
基础配置可以查看 深入理解TypeScript - 认识TypeScript&配置详解 。
除了compilerOptions属性外,还可以配置另外两个属性来提高编译速度。这样就不会再去编译node_modules中的文件。
"include": [
"src"
],
"exclude": [
"node_modules",
]
skipLibCheck
跳过所有声明文件的类型检查(*.d.ts),声明文件不做类型校验。
noImplicitAny
noImplicitAny:true,使用简单的any类型会发出警告,默认为false。这意味着,在编写代码时,您需要更多地关注类型,如果不指定类型,编译器就总是会「抱怨」。
props没有指定类型,默认是any类型,编译会报错。
noImplicitAny:false,依然会出现红色波浪线,但是编译不会报错。
显示的定义为any,报错将会消失:
allowSyntheticDefaultImports
为true,允许引入没有默认导出的模块。
如果为false,引入React就需要用下面的方式引入,这样所有的导出多加在了React对象上面:
import * as React from 'react';
函数组件
你可以使用 React.FunctionComponent
接口定义函数组件:
写一个简单的函数组件:
mport * as React from 'react';
import './App.css';
const App = (props) => { // Parameter 'props' implicitly has an 'any' type.
return (
<div className="App">
Learn React { props.name}
</div>)
}
export default App;
这么写会发现编译报错,主要是因为tsconfig.json中配置不允许隐式的any类型,你可以显式定义为any, (props: any).。或者用接口来定义props类型:
interface IProps {
name: string;
}
(props: IProps) => ...这样就可以不会再出现编译报错了,但是index.tsx文件给出了提示:
这是因为再App组件props属性上,name属性是必须存在的,所以这里必须要传递name属性,但是有的时候我们在写一个组件的时候,有些props是可以选择传递或者不传递的,这个时候就主要这样来定义接口:
interface IProps {
name?: string;
}
一般情况下,props都需要哪些属性,开发人员都是很清楚的,所以只要把你需要的属性都定义在接口中,哪些是必须传递的,哪些是不必须传递的,都可以很明白的定义出来,这样如果一些必须传递的属性在使用的过程中漏传,编译器马上会给你指出来,这样就避免了在允许阶段的报错,提高了开发效率。
也可以用下面的方式来定义函数组件:
interface IProps {
name?: string;
}
const App: React.FunctionComponent<IProps> = props => {
return (
<div className="App">
Learn React { props.name}
</div>)
}
类组件
import * as React from 'react';
import './App.css';
interface IProps {
name?: string;
}
interface IState {
num: number;
}
class App extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
num: 8
}
}
render() {
return(
<div className="App">
Learn React { this.props.name}
</div>)
}
}
export default App;
<App />
props属性的值可以是字符串,也可以是JSX, 如果是JSX,类型为React.ReactNode:
import * as React from 'react';
import './App.css';
interface IProps {
header?: React.ReactNode;
}
const App: React.FunctionComponent<IProps> = props => { // Parameter 'props' implicitly has an 'any' type.
return (
<div className="App">
Learn React { props.header}
</div>)
}
export default App;
<App header={<h1>header</h1>} />
react 类型声明文件提供了 React.ReactElement<T>
,它可以让你通过传入 <T/>
,来注解类组件的实例化结果。将App组件赋值给另外一个变量:
const otherApp: React.ReactElement<App> = <App />
泛型组件
如果说一个组件接收的参数是一个number类型的数组,可以这样来定义props类型:
type IProp = { items: number[] };
如果是sting类型的数组,可以这样来定义:
type IProp = { items: string[] };
但是,有的时候传递的数组类型不固定,我们希望如果传递的是sting类型,items中类型是sting[],如果是number类型,items的类型是number[]。这就跟前面章节中讲到的泛型类似,所以这里就可以以这样的方式来定义一个泛型组件:
// 一个泛型组件
type SelectProps<T> = { items: T[] };
class Select<T> extends React.Component<SelectProps<T>, any> {}
// 使用
const Form = () => <Select<string> items={['a', 'b']} />;
受控组件
写一个input的onChange事件,需要通过事件对象来获取value,怎样获取e的正确类型呢?
<input
type="text"
name="ming"
value={this.state.ming}
onChange={this.handleChange}
/>
handleChange = (e) => {
this.setState({
xing: e.target.value
// [e.target.name]: e.target.value
})
}
鼠标放置上去,会有类型提示:
受控组件完整代码:
import React, { Component } from 'react'
type IProps = {}
type IState = {
xing: string;
ming: string;
[key: string]: string;
}
export default class MyInput extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
xing: '',
ming: ''
}
}
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
// xing: e.target.value
[e.target.name]: e.target.value
})
}
render() {
return (
<div>
<label htmlFor="">姓:
<input type="text"
value={this.state.xing}
name="xing"
onChange={this.handleChange}
/>
</label>
<label htmlFor="">
名:
<input
type="text"
name="ming"
value={this.state.ming}
onChange={this.handleChange}
/>
</label>
<p>
{this.state.xing} {this.state.ming}
</p>
</div>
)
}
}
非受控组件
完整代码:
import React, { FormEvent } from 'react'
const UserForm = () => {
let userRef: React.RefObject<HTMLInputElement> = React.createRef()
let pwdInput: HTMLInputElement | null;
let handleSubmit = (e: FormEvent) => {
e.preventDefault();
console.log('submit')
console.log(userRef.current?.value)
console.log(userRef.current!.value)
console.log((userRef.current as HTMLInputElement).value)
console.log(pwdInput?.value)
}
return (
<form action="" onSubmit={handleSubmit}>
username: <input ref={userRef}></input>
pwd: <input ref={(node) => { return pwdInput = node; }}></input>
<button type="submit">提交</button>
</form>
)
}
export default UserForm;
高阶组件
react提供了一个ComponentType类型来定义组件类型:
import React, { Component, ComponentType } from 'react'
class App extends Component {
render() {
return (
<div>
<h1>
react
</h1>
<p>
React.js 是一个构件用户界面的库
</p>
</div>
)
}
}
const withCopyright = (WrappedComponent: ComponentType) => {
return class extends Component {
render() {
return (
<>
<WrappedComponent></WrappedComponent>
<div>@copyright; 版权所有 NZ2020</div>
</>
)
}
}
}
const CopyrightApp = withCopyright(App);
export default CopyrightApp;
button点击
import React, { ReactNode } from 'react'
type IProps = {
click(e: React.MouseEvent): void,
children: ReactNode
}
const Button = (props: IProps) => {
return (
<button onClick={props.click}>{props.children}</button>
)
}
export default Button;
<Button
click={this.handleClick}
>
<span>button test</span>
</Button>
封装请求函数
src/utils/index.ts
这个函数返回了一个new Promise,就可以给这个函数一个Promise类型,鼠标放上去会看到类型提示,说是Promise类型需要接收一个 参数。这个参数代表的是resolve参数类型。可以定义个resolve参数类型接口。
初步实现代码如下:
interface IResponse {
message: string;
result: [];
success: boolean;
}
function request(): Promise<IResponse> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
message: '',
result: [],
success: true,
})
})
})
}
export default request
resolve参数一般result属性一般情况下不会固定,有时是个对象,有时是个数组,数组还会有不同的类型,所以Promise类型接口还提供了另外一个泛型,可以动态的定义result的类型:
interface IResponse<T> {
message: string;
result: T;
success: boolean;
}
type IParams = {
url: string;
method: 'get' | 'post',
data?: any
}
function request(): Promise<IResponse<number[]>> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
message: '',
result: [3,9],
success: true,
})
})
})
}
export default request
配置store
actions.ts
export type Payload = {
name: string;
}
export function addAction(params: Payload) {
return {
type: 'ADD',
payload: params
};
}
reducer.ts
import { Reducer } from "redux"
import { Payload } from './actions'
interface State {
name: string
}
interface Action<T> {
type: string;
payload: T
}
export const reducers: Reducer<State, Action<Payload>> = (state = {name: '麦乐'}, action) => {
switch (action.type) {
case "ADD":
return {
...state,
...action.payload
}
default:
return {
...state,
}
}
}
index.ts
import { createStore } from 'redux'
import { reducers } from './reducer'
const store = createStore(reducers)
export default store;