TypeScript在react中的實踐

TypeScript 是 JS 類型的超集,並支持了泛型、類型、命名空間、枚舉等特性,彌補了 JS 在大型應用開發中的不足,本文主要探索在 TypeScript版本中編寫 React 組件的姿式。javascript

在動手將TypeScript融合進現有的React項目以前,先看一下create-react-app是怎麼作的。html

從create-react-app中一探究竟

首先建立一個叫作my-app的新工程:java

create-react-app my-app --scripts-version=react-scripts-ts
複製代碼

react-scripts-ts是一系列適配器,它利用標準的create-react-app工程管道並把TypeScript混入進來。此時的工程結構應以下所示:node

my-app/
├─ .gitignore
├─ node_modules/
├─ public/
├─ src/
│  └─ ...
├─ package.json
├─ tsconfig.json
└─ tslint.json
複製代碼

注意:react

  • tsconfig.json包含了工程裏TypeScript特定的配置選項。
  • tslint.json保存了要使用的代碼檢查器的設置,TSLint
  • package.json包含了依賴,還有一些命令的快捷方式,如測試命令,預覽命令和發佈應用的命令。
  • public包含了靜態資源如HTML頁面或圖片。除了index.html文件外,其它的文件均可以刪除。
  • src包含了TypeScript和CSS源碼。index.tsx是強制使用的入口文件。

@types

打開package.json文件,查看devDependencies,發現一系列@types文件,以下:webpack

"devDependencies": {
    "@types/node": "^12.6.9",
    "@types/react": "^16.8.24",
    "@types/react-dom": "^16.8.5",
    "typescript": "^3.5.3"
}
複製代碼

使用@types/前綴表示咱們額外要獲取React和React-DOM的聲明文件(關於聲明文件,參考文章)。 一般當你導入像"react"這樣的路徑,它會查看react包; 然而,並非全部的包都包含了聲明文件,因此TypeScript還會查看@types/react包。git

若是沒有這些@types文件,咱們在TSX 組件中,引入React 或者ReactDOM 會報錯:es6

Cannot find module 'react'github

Cannot find module 'react-dom'web

錯誤緣由是因爲 ReactReact-dom 並非使用 TS 進行開發的,因此 TS 不知道 ReactReact-dom 的類型,以及該模塊導出了什麼,此時須要引入 .d.ts 的聲明文件,比較幸運的是在社區中已經發布了這些經常使用模塊的聲明文件 DefinitelyTyped

因此若是咱們的工程不是使用create-react-app建立的,記得npm install @types/xxx

tsconfig.json

若是一個目錄下存在一個tsconfig.json文件,那麼它意味着這個目錄是TypeScript項目的根目錄。tsconfig.json文件中指定了用來編譯這個項目的根文件和編譯選項。

執行tsc --init生成本身的tsconfig.json配置文件,示例以下。

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react"
    },
    "include": [
        "./src/**/*"
    ]
}
複製代碼
  • target:默認狀況下,編譯目標是 es5,若是你只想發佈到兼容 es6 的瀏覽器中,也能夠把它配置爲 es6。 不過,若是配置爲 es6,那麼一些老的瀏覽器(如 IE )中就會拋出 Syntax Error 錯誤。
  • noImplicitAny :當 noImplicitAny 標誌是 false(默認值)時, 若是編譯器沒法根據變量的用途推斷出變量的類型,它就會悄悄的把變量類型默認爲 any。這就是隱式 any的含義。當 noImplicitAny 標誌是 true 而且 TypeScript 編譯器沒法推斷出類型時,它仍然會生成 JavaScript 文件。 可是它也會報告一個錯誤

使用eslint進行代碼檢查

安裝 eslint 依賴

npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy  babel-eslint --save-dev
複製代碼
  1. @typescript-eslint/parser:將 TypeScript 轉換爲 ESTree,使 eslint 能夠識別
  2. @typescript-eslint/eslint-plugin:只是一個能夠打開或關閉的規則列表

建立配置文件.eslintrc.js並寫入規則

module.exports = {
    parser: "@typescript-eslint/parser",
 	extends: ["plugin:@typescript-eslint/recommended", "react-app"],
  	plugins: ["@typescript-eslint", "react"],
  	rules: {
        // ...
    }
}
複製代碼

這裏使用的是 AlloyTeam ESLint 的 TypeScript 規則

而後在package.json中增長配置,檢查src目錄下全部的ts文件。

"scripts": {
	"eslint": "eslint src --ext .ts,.js,.tsx,.jsx"
}
複製代碼

此時執行 npm run eslint 即會檢查 src 目錄下的全部.ts,.js,.tsx,.jsx後綴的文件

安裝 prettier 依賴

npm i prettier eslint-config-prettier eslint-plugin-prettier -D
複製代碼
  1. prettier: 格式化規則程序
  2. eslint-config-prettier: 將禁用任何可能干擾現有 prettier 規則的 linting 規則
  3. eslint-plugin-prettier: 將做爲ESlint 的一部分運行 Prettier分析。
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  plugins: ['@typescript-eslint', 'react'],
  rules: {},
};
複製代碼

舊項目中引入Prettier會致使超級多的error,慎用

Visual Studio Code 集成 ESLint 與 Prettier

爲了讓 vscode 的 eslint 插件啓用 typescript 支持,須要添加下面的配置到 .vscode/settings.json 中。

"eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "typescript",
      "autoFix": true
    },
    {
      "language": "typescriptreact",
      "autoFix": true
    }
]
複製代碼

在webpack中配置loader

修改webpack.config.js文件

module.exports = {
    entry: "./src/index.tsx",
    output: {
        filename: "bundle.js",
        path: __dirname + "/dist"
    },

    devtool: "source-map",

    resolve: {
        extensions: [".ts", ".tsx", ".js", ".json"]
    },

    module: {
        rules: [
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
            { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
        ]
    },
    externals: {
        "react": "React",
        "react-dom": "ReactDOM"
    },
};
複製代碼

awesome-typescript-loader是用來編譯ts文件得,也可使用ts-loader,二者之間得區別,請參考:awesome-typescript-loader & ts-loader

組件開發

有狀態組件開發

定義interface

當咱們傳遞props到組件中去的時候,若是想要使props應用interface,那就會強制要求咱們傳遞的props必須遵循interface的結構,確保成員都有被聲明,同時也會阻止未指望的props被傳遞下去。

interface能夠定義在組件的外部或是一個獨立文件,能夠像這樣定義一個interface

interface FormProps {
    first_name: string;
    last_name: string;
    age: number;
    agreetoterms?: boolean;
}
複製代碼

這裏咱們建立了一個FormProps接口,包含一些值。咱們也能夠給組件的state應用一個interface

interface FormState {
    submitted?: boolean;
    full_name: string;
    age: number;
}
複製代碼

給組件應用interface

咱們既能夠給類組件也能夠給無狀態組件應用interface。對於類組件,咱們利用尖括號語法去分別應用咱們的props和state的interface。

export class MyForm extends React.Component<FormProps, FormState> {
	...
}
複製代碼

注意:在只有state而沒有props的狀況下,props的位置能夠用{}或者object佔位,這兩個值都表示有效的空對象。

對於純函數組件,咱們能夠直接傳遞props interface

function MyForm(props: FormProps) {
	...
}
複製代碼

引入interface

按照約定,咱們通常會建立一個 **src/types/**目錄來將你的全部interface分組:

// src/types/index.tsx
export interface FormProps {
    first_name: string;
    last_name: string;
    age: number;
    agreetoterms?: boolean;
}
複製代碼

而後引入組件所須要的interface

// src/components/MyForm.tsx
import React from 'react';
import { StoreState } from '../types/index';
...
複製代碼

無狀態組件開發

無狀態組件也被稱爲展現組件,若是一個展現組件沒有內部的state能夠被寫爲純函數組件。 若是寫的是函數組件,在@types/react中定義了一個類型type SFC<P = {}> = StatelessComponent<P>;。咱們寫函數組件的時候,能指定咱們的組件爲SFC或者StatelessComponent。這個裏面已經預約義了children等,因此咱們每次就不用指定類型children的類型了。

實現源碼 node_modules/@types/react/index.d.ts

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;
}
複製代碼

使用 SFC 進行無狀態組件開發。

import React, { ReactNode, SFC } from 'react';
import style from './step-complete.less';

export interface IProps  {
  title: string | ReactNode;
  description: string | ReactNode;
}
const StepComplete:SFC<IProps> = ({ title, description, children }) => {
  return (
    <div className={style.complete}> <div className={style.completeTitle}> {title} </div> <div className={style.completeSubTitle}> {description} </div> <div> {children} </div> </div>
  );
};
export default StepComplete;
複製代碼

事件處理

咱們在進行事件註冊時常常會在事件處理函數中使用 event 事件對象,例如當使用鼠標事件時咱們經過 clientXclientY 去獲取指針的座標。

你們能夠想到直接把 event 設置爲 any 類型,可是這樣就失去了咱們對代碼進行靜態檢查的意義。

function handleEvent (event: any) {
  console.log(event.clientY)
}
複製代碼

試想下當咱們註冊一個 Touch 事件,而後錯誤的經過事件處理函數中的 event 對象去獲取其 clientY 屬性的值,在這裏咱們已經將 event 設置爲 any 類型,致使 TypeScript 在編譯時並不會提示咱們錯誤, 當咱們經過 event.clientY 訪問時就有問題了,由於 Touch 事件的 event 對象並無 clientY 這個屬性。

經過 interfaceevent 對象進行類型聲明編寫的話又十分浪費時間,幸運的是 React 的聲明文件提供了 Event 對象的類型聲明。

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> 過渡事件對象

實例:

import { MouseEvent } from 'react';

interface IProps {
  onClick (event: MouseEvent<HTMLDivElement>): void,
}
複製代碼

Promise 類型

在作異步操做時咱們常用 async 函數,函數調用時會 return 一個 Promise 對象,可使用 then 方法添加回調函數。

Promise<T> 是一個泛型類型,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)
  })
複製代碼

咱們首先聲明 IResponse 的泛型接口用於定義 response 的類型,經過 T 泛型變量來肯定 result 的類型。

而後聲明瞭一個 異步函數 getResponse 而且將函數返回值的類型定義爲 Promise<IResponse<number[]>>

最後調用 getResponse 方法會返回一個 promise 類型,經過 then 調用,此時 then 方法接收的第一個回調函數的參數 response 的類型爲,{ message: string, result: number[], success: boolean}

泛型組件

工具泛型使用技巧

typeof

通常咱們都是先定義類型,再去賦值使用,可是使用 typeof 咱們能夠把使用順序倒過來。

const options = {
  a: 1
}
type Options = typeof options
複製代碼

使用字符串字面量類型限制值爲固定的字符串參數

限制 props.color 的值只能夠是字符串 redblueyellow

interface IProps {
  color: 'red' | 'blue' | 'yellow',
}
複製代碼

使用數字字面量類型限制值爲固定的數值參數

限制 props.index 的值只能夠是數字 012

interface IProps {
 index: 0 | 1 | 2,
}
複製代碼

使用 Partial 將全部的 props 屬性都變爲可選值

Partial` 實現源碼 `node_modules/typescript/lib/lib.es5.d.ts
type Partial<T> = { [P in keyof T]?: T[P] };
複製代碼

上面代碼的意思是 keyof T 拿到 T 全部屬性名, 而後 in 進行遍歷, 將值賦給 P , 最後 T[P] 取得相應屬性的值,中間的 ? 用來進行設置爲可選值。

若是 props 全部的屬性值都是可選的咱們能夠藉助 Partial 這樣實現。

import { MouseEvent } from 'react'
import * as React from 'react'
interface IProps {
  color: 'red' | 'blue' | 'yellow',
  onClick (event: MouseEvent<HTMLDivElement>): void,
}
const Button: SFC<Partial<IProps>> = ({onClick, children, color}) => {
  return (
    <div onClick={onClick}>
      { children }
    </div>
  )
複製代碼

使用 Required 將全部 props 屬性都設爲必填項

Required 實現源碼 node_modules/typescript/lib/lib.es5.d.ts

type Required<T> = { [P in keyof T]-?: T[P] };
複製代碼

看到這裏,小夥伴們可能有些疑惑, -? 是作什麼的,其實 -? 的功能就是把可選屬性的 ? 去掉使該屬性變成必選項,對應的還有 +? ,做用與 -? 相反,是把屬性變爲可選項。

條件類型

TypeScript2.8引入了條件類型,條件類型能夠根據其餘類型的特性作出類型的判斷。

T extends U ? X : Y
複製代碼

原先

interface Id { id: number, /* other fields */ }
interface Name { name: string, /* other fields */ }
declare function createLabel(id: number): Id;
declare function createLabel(name: string): Name;
declare function createLabel(name: string | number): Id | Name;
複製代碼

使用條件類型

type IdOrName<T extends number | string> = T extends number ? Id : Name;
declare function createLabel<T extends number | string>(idOrName: T): T extends number ? Id : Name;
複製代碼

Exclude<T,U>

T 中排除那些能夠賦值給 U 的類型。

Exclude 實現源碼 node_modules/typescript/lib/lib.es5.d.ts

type Exclude<T, U> = T extends U ? never : T;
複製代碼

實例:

type T = Exclude<1|2|3|4|5, 3|4>  // T = 1|2|5 
複製代碼

此時 T 類型的值只能夠爲 125 ,當使用其餘值是 TS 會進行錯誤提示。

Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.
複製代碼

Extract<T,U>

T 中提取那些能夠賦值給 U 的類型。

Extract實現源碼 node_modules/typescript/lib/lib.es5.d.ts

type Extract<T, U> = T extends U ? T : never;
複製代碼

實例:

type T = Extract<1|2|3|4|5, 3|4>  // T = 3|4
複製代碼

此時T類型的值只能夠爲 34 ,當使用其餘值時 TS 會進行錯誤提示:

Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.
複製代碼

Pick<T,K>

T 中取出一系列 K 的屬性。

Pick 實現源碼 node_modules/typescript/lib/lib.es5.d.ts

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
複製代碼

實例:

假如咱們如今有一個類型其擁有 nameagesex 屬性,當咱們想生成一個新的類型只支持 nameage 時能夠像下面這樣:

interface Person {
  name: string,
  age: number,
  sex: string,
}
let person: Pick<Person, 'name' | 'age'> = {
  name: '小王',
  age: 21,
}
複製代碼

Record<K,T>

K 中全部的屬性的值轉化爲 T 類型。

Record 實現源碼 node_modules/typescript/lib/lib.es5.d.ts

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
複製代碼

實例:

nameage 屬性所有設爲 string 類型。

let person: Record<'name' | 'age', string> = {
  name: '小王',
  age: '12',
}
複製代碼

Omit<T,K>(沒有內置)

從對象 T 中排除 keyK 的屬性。

因爲 TS 中沒有內置,因此須要咱們使用 PickExclude 進行實現。

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
複製代碼

實例:

排除 name 屬性。

interface Person {
  name: string,
  age: number,
  sex: string,
}


let person: Omit<Person, 'name'> = {
  age: 1,
  sex: '男'
}
複製代碼

NonNullable<T>

排除 Tnullundefined

NonNullable 實現源碼 node_modules/typescript/lib/lib.es5.d.ts

type NonNullable<T> = T extends null | undefined ? never : T;
複製代碼

實例:

type T = NonNullable<string | string[] | null | undefined>; // string | string[]
複製代碼

ReturnType<T>

獲取函數 T 返回值的類型。。

ReturnType 實現源碼 node_modules/typescript/lib/lib.es5.d.ts

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
複製代碼

infer R 至關於聲明一個變量,接收傳入函數的返回值類型。

實例:

type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<(s: string) => void>; // void
複製代碼
相關文章
相關標籤/搜索