TypeScript 是 JS 類型的超集,並支持了泛型、類型、命名空間、枚舉等特性,彌補了 JS 在大型應用開發中的不足,本文主要探索在 TypeScript版本中編寫 React 組件的姿式。javascript
在動手將TypeScript融合進現有的React項目以前,先看一下create-react-app
是怎麼作的。html
首先建立一個叫作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
是強制使用的入口文件。打開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
錯誤緣由是因爲 React
和 React-dom
並非使用 TS 進行開發的,因此 TS 不知道 React
、 React-dom
的類型,以及該模塊導出了什麼,此時須要引入 .d.ts 的聲明文件,比較幸運的是在社區中已經發布了這些經常使用模塊的聲明文件 DefinitelyTyped 。
因此若是咱們的工程不是使用
create-react-app
建立的,記得npm install @types/xxx
。
若是一個目錄下存在一個tsconfig.json
文件,那麼它意味着這個目錄是TypeScript項目的根目錄。tsconfig.json
文件中指定了用來編譯這個項目的根文件和編譯選項。
執行tsc --init
生成本身的tsconfig.json
配置文件,示例以下。
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react"
},
"include": [
"./src/**/*"
]
}
複製代碼
noImplicitAny
標誌是 false
(默認值)時, 若是編譯器沒法根據變量的用途推斷出變量的類型,它就會悄悄的把變量類型默認爲 any
。這就是隱式 any的含義。當 noImplicitAny
標誌是 true
而且 TypeScript 編譯器沒法推斷出類型時,它仍然會生成 JavaScript 文件。 可是它也會報告一個錯誤。npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy babel-eslint --save-dev
複製代碼
@typescript-eslint/parser
:將 TypeScript 轉換爲 ESTree,使 eslint 能夠識別@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後綴的文件
npm i prettier eslint-config-prettier eslint-plugin-prettier -D
複製代碼
prettier
: 格式化規則程序eslint-config-prettier
: 將禁用任何可能干擾現有 prettier
規則的 linting
規則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,慎用
爲了讓 vscode 的 eslint 插件啓用 typescript 支持,須要添加下面的配置到 .vscode/settings.json
中。
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
}
]
複製代碼
修改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
當咱們傳遞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。對於類組件,咱們利用尖括號語法去分別應用咱們的props和state的interface。
export class MyForm extends React.Component<FormProps, FormState> {
...
}
複製代碼
注意:在只有state而沒有props的狀況下,props的位置能夠用{}或者object佔位,這兩個值都表示有效的空對象。
對於純函數組件,咱們能夠直接傳遞props interface
function MyForm(props: FormProps) {
...
}
複製代碼
按照約定,咱們通常會建立一個 **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
事件對象,例如當使用鼠標事件時咱們經過 clientX
、clientY
去獲取指針的座標。
你們能夠想到直接把 event
設置爲 any
類型,可是這樣就失去了咱們對代碼進行靜態檢查的意義。
function handleEvent (event: any) {
console.log(event.clientY)
}
複製代碼
試想下當咱們註冊一個 Touch
事件,而後錯誤的經過事件處理函數中的 event
對象去獲取其 clientY
屬性的值,在這裏咱們已經將 event
設置爲 any
類型,致使 TypeScript 在編譯時並不會提示咱們錯誤, 當咱們經過 event.clientY
訪問時就有問題了,由於 Touch
事件的 event
對象並無 clientY
這個屬性。
經過 interface
對 event
對象進行類型聲明編寫的話又十分浪費時間,幸運的是 React 的聲明文件提供了 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,
}
複製代碼
在作異步操做時咱們常用 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
咱們能夠把使用順序倒過來。
const options = {
a: 1
}
type Options = typeof options
複製代碼
限制 props.color
的值只能夠是字符串 red
、blue
、yellow
。
interface IProps {
color: 'red' | 'blue' | 'yellow',
}
複製代碼
限制 props.index
的值只能夠是數字 0
、 1
、 2
。
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;
複製代碼
從 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
類型的值只能夠爲 1
、2
、 5
,當使用其餘值是 TS 會進行錯誤提示。
Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.
複製代碼
從 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類型的值只能夠爲 3
、4
,當使用其餘值時 TS 會進行錯誤提示:
Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.
複製代碼
從 T
中取出一系列 K
的屬性。
Pick
實現源碼 node_modules/typescript/lib/lib.es5.d.ts
。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
複製代碼
實例:
假如咱們如今有一個類型其擁有 name
、 age
、 sex
屬性,當咱們想生成一個新的類型只支持 name
、age
時能夠像下面這樣:
interface Person {
name: string,
age: number,
sex: string,
}
let person: Pick<Person, 'name' | 'age'> = {
name: '小王',
age: 21,
}
複製代碼
將 K
中全部的屬性的值轉化爲 T
類型。
Record
實現源碼 node_modules/typescript/lib/lib.es5.d.ts
。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
複製代碼
實例:
將 name
、 age
屬性所有設爲 string
類型。
let person: Record<'name' | 'age', string> = {
name: '小王',
age: '12',
}
複製代碼
從對象 T
中排除 key
是 K
的屬性。
因爲 TS 中沒有內置,因此須要咱們使用 Pick
和 Exclude
進行實現。
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: '男'
}
複製代碼
排除 T
爲 null
、undefined
。
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[]
複製代碼
獲取函數 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
複製代碼