TypeScript小抄寫員

大量摘抄於各種文章 ———— 站在巨人的肩膀上搬磚

TypeScript官方文檔
(中文版)
是最好的學習材料html

TS是什麼

TypeScript = Type + Script(標準JS)。咱們從TS的官方網站上就能看到定義:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript。TypeScript是一個編譯到純JS的有類型定義的JS超集。react

爲何要用TS

  • 類型安全,能夠類比 Java。
  • TS 面向對象理念,支持面向對象的封裝、繼承、多態三大特性
  • 相似 babel,ES6 ES7 新語法均可以寫,最終 TS 會進行編譯。
  • 生產力工具的提高,VS Code + TS 使 IDE 更容易理解你的代碼
  • 給應用配置、應用狀態、先後端接口及各類模塊定義類型,使協做更爲方便、高效和安全,整個應用就是類型定義文檔
  • 類型系統+靜態分析檢查+智能感知/提示,使大規模的應用代碼質量更高,運行時bug更少,更方便維護和重構

靜態類型、動態類型和弱類型、強類型

  • 靜態類型:編譯期就知道每個變量的類型。類型錯誤編譯失敗是語法問題。如Java、C++。
  • 動態類型:編譯期不知道類型,運行時才知道。類型錯誤拋出異常發生在運行時。如JS、Python。
  • 弱類型:容忍隱式類型轉換。如JS,1+'1'='11',數字型轉成了字符型。
  • 強類型:不容忍隱式類型轉換。如Python,1+'1'會拋出TypeError。

權衡

如何更好的利用JS的動態性和TS的靜態特質,咱們須要結合項目的實際狀況來進行綜合判斷。一些建議:webpack

  • 若是是中小型項目,且生命週期不是很長,那就直接用JS吧,不要被TS束縛住了手腳。
  • 若是是大型應用,且生命週期比較長,那建議試試TS。開源項目如VS CodeGitHub桌面端
  • 若是是框架、庫之類的公共模塊,那更建議用TS了。

至於到底用不用TS,仍是要看實際項目規模、項目生命週期、團隊規模、團隊成員狀況等實際狀況綜合考慮。git

TS能幹什麼

靜態檢查

低級錯誤、非空判斷、類型推斷,這類問題是ESLint等工具檢測不出來的。
  • 基礎類型
let isDone: boolean = false;

let decimal: number = 6;

let color: string = "blue";

// 數組,有兩種寫法
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];

// 元組(Tuple)
let x: [string, number] = ["hello", 10];

// 枚舉
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

// 不肯定的能夠先聲明爲any
let notSure: any = 4;

// 聲明沒有返回值
function warnUser(): void {
    alert("This is my warning message");
}

let u: undefined = undefined;

let n: null = null;

// 類型永遠沒返回
function error(message: string): never {
    throw new Error(message);
}

// 類型主張,就是知道的比編譯器多,主動告訴編譯器更多信息,有兩種寫法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;

面向對象編程加強

  • 訪問權限控制

信息隱藏有助於更好的管理系統的複雜度,這在軟件工程中顯得尤其重要。github

class Person {
  protected name: string;
  public age: number;
  constructor(name: string) { this.name = name; }
}

class Employee extends Person {
  static someAttr = 1;
  private department: string;

  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }
}
let howard = new Employee("Howard", "Sales");
console.log(howard.name);
// 報錯:Person中name屬性是protected類型,只能在本身類中或者子類中使用
  • 接口 interface

Robot類能夠繼承Base類,並實現Machine和Human接口,
這種能夠組合繼承類和實現接口的方式使面向對象編程更爲靈活、可擴展性更好。web

interface Machine {
  move(): void
}

interface Human {
  run(): void
}

class Base {
}

class Robot extends Base implements Machine, Human {
  run() {
    console.log('run');
  }
  move() {
    console.log('move');
  }
}
  • 泛型

定義了一個模板類型T,實例化GenericNumber類時能夠傳入內置類型或者自定義類型。泛型(模板)在傳統面向對象編程語言中是很常見的概念了,在代碼邏輯是通用模式化的,參數能夠是動態類型的場景下比較有用。typescript

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
  • 類型系統

定義了一個系統配置類型SystemConfig和一個模塊類型ModuleType,咱們在使用這些類型時就不能隨便修改config和mod的數據了,這對於多人協做的團隊項目很是有幫助。npm

interface SystemConfig {
  attr1: number;
  attr2: string;
  func1(): string;
}

interface ModuleType {
  data: {
    attr1?: string,
    attr2?: number
  },
  visible: boolean
}

const config: SystemConfig = {
  attr1: 1,
  attr2: 'str',
  func1: () => ''
};

const mod: ModuleType = {
  data: {
    attr1: '1'
  },
  visible: true
};
  • 模塊系統加強 module/namespace

TS除了支持ES6的模塊系統以外,還支持命名空間。這在管理複雜模塊的內部時比較有用。編程

namespace N {
  export namespace NN {
    export function a() {
      console.log('N.a');
    }
  }
}

N.NN.a();

面向對象相關概念詳見json

vs Babel

  • Web和Node平臺的JS始終與JS最新規範有一段距離,Web平臺的距離更遠,TS能夠填充這個間隙,讓使用者在Web和Node平臺都能用上最新的Feature,用上優雅的JS,提升生產力。
  • Babel也是很不錯的ES6 to 5編譯工具,有不錯的插件機制,社區發展也不錯,但在一樣一段代碼編譯出的JS代碼裏能夠看到,TS編譯後的代碼是更符合習慣、簡潔易讀一些(都用的是官方網站的Playground工具)。

使用TS的成本

老項目

對於老項目,因爲TS兼容ES規範,因此能夠比較方便的升級現有的JS(這裏指ES6及以上)代碼,逐漸的加類型註解,漸進式加強代碼健壯性。遷移過程:

  1. npm全局安裝typescript包,並在工程根目錄運行tsc --init,自動產生tsconfig.json文件。 默認的3個配置項:更多配置項說明
  • "target":"es5":編譯後代碼的ES版本,還有es3,es2105等選項。
  • "module":"commonjs":編譯後代碼的模塊化組織方式,還有amd,umd,es2015等選項。
  • "strict":true:嚴格校驗,包含不能有沒意義的any,null校驗等選項。
  1. 初始化獲得的tsconfig.json無需修改,增長"allowJs": true選項。
  2. 配置webpack配置,增長ts的loader,如awesome-typescript-loader。
loaders: [
    // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
    { test: /\.tsx?$/, loader: "awesome-typescript-loader" }
]
  1. 此時你能夠寫文件名爲ts和tsx後綴的代碼了,它能夠和現有的ES6代碼共存,重構之前的ES6代碼爲TS代碼,只需將文件後綴改爲ts(x)就行,就能夠享受TS及IDE智能感知/糾錯帶來的好處。

tsconfig.json配置參閱

新項目

對於新項目,微軟提供了很是棒的一些Starter項目,詳細介紹瞭如何用TS和其餘框架、庫配合使用。若是是React項目,能夠參考這個Starter:TypeScript-React-Starter

周邊生態

類型聲明包

React、及其餘各類著名框架、庫都有TS類型聲明,咱們能夠在項目中經過npm install @types/react方式安裝,能夠在這個網站搜索你想要安裝的庫聲明包。安裝後,寫和那些框架、庫相關的代碼將會是一種很是爽的體驗,函數的定義和註釋將會自動提示出來,開發效率將會獲得提高。

TS進階

類型別名type

  • 至關於自定義一個類型
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

交叉類型 ( & )

  • 交叉類型 intersection types是將多個類型合併成一個類型
type Person = Huaren & Bairen & Heiren;

聯合類型 ( | )

  • 聯合類型(Union Types)表示取值能夠爲多種類型中的一種
  • 當 TypeScript 不肯定一個聯合類型的變量究竟是哪一個類型的時候,咱們只能訪問此聯合類型的全部類型裏共有的屬性或方法:
function getLength(something: string | number): number {
    return something.length;❌
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

function getString(something: string | number): string {
    return something.toString();✅
}

字符串字面量類型

  • 字符串字面量類型用來約束取值只能是某幾個字符串中的一個
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
    // do something
}

handleEvent(document.getElementById('hello'), 'scroll');  // 沒問題
handleEvent(document.getElementById('world'), 'dblclick'); // 報錯,event 不能爲 'dblclick'

// index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.

類型保護

  • 當咱們須要在還不肯定類型的時候就訪問其中一個類型特有的屬性或方法,就要用到類型保護

使用類型斷言

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}
function isFish(animal: Cat | Fish) {
    if (typeof animal.swim === 'function'❌) {
        return true;
    }
    return false;
}
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
//   Property 'swim' does not exist on type 'Cat'.

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish✅).swim === 'function') {
        return true;
    }
    return false;
}

使用類型守衛

類型謂詞守衛自定義類型

  • 返回值animal is Fish就是類型謂詞
  • 謂詞形式爲parameterName is Type,parameterName必須是來自於當前函數簽名裏的參數名
function isFish(animal: Fish | Bird): animal is Fish {
    return (animal as Fish).swim !== undefined;
}

typeof類型守衛

  • 只有typeof v === "typename"typeof v !== "typename"兩種形式能被識別
  • typeof 只能用於 "number", "string", "boolean", "symbol"
  • TypeScript並不會阻止你與其它字符串比較,語言不會把那些表達式識別爲類型守衛

instanceof類型守衛

  • instanceof用於守護類
function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 類型爲SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 類型細化爲'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 類型細化爲'StringPadder'
}

泛型約束

  • 使用 extends 約束泛型 T 必須符合接口 Lengthwise 的形狀,也就是必須包含 length 屬性
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

索引類型

  • 索引類型查詢操做符 ( keyof )
  • 索引訪問操做符 ( T[K] )
  • 對於任何類型 T, keyof T的結果爲 T上已知的公共屬性名聯合的字符串字面量類型

舉個例子:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key]
}
interface IObj {
    name: string;
    age: number;
    male: boolean;
}
const obj:IObj = {
    name: 'zhangsan',
    age: 18,
    male: true
}
let x1 = getProperty(obj, 'name') // 容許,x1的類型爲string
let x2 = getProperty(obj, 'age') // 容許,x2的類型爲number
let x3 = getProperty(obj, 'male') // 容許,x3的類型爲boolean
let x4 = getProperty(obj, 'sex') // 報錯:Argument of type '"sex"' is not 
// assignable to parameter of type '"name" | "age" | "male"'.
  1. 上述例子,定義了一個getProperty函數,來獲取指定對象的指定屬性
  2. 首先,使用keyof關鍵字,得到泛型T上已知的公共屬性名聯合的字符串字面量類型'name' | 'age' | 'male'
  3. 而後,使用泛型約束K extends keyof T限制K只能是'name' | 'age' | 'male'中的一個值
  4. T[K]則表明對象裏對應key的元素的類型

更好的理解索引類型

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}
// T[K][]也能夠寫成 Array<T[K]>
interface Person {
    name: string;
    age: number;
    sex: string;
}
let person: Person = {
    name: 'Jarid',
    age: 35,
    sex: '男',
};
let strings: string[] = pluck(person, ['name', 'sex']); // ok, string[], [ 'Jarid', '男' ]
let numbers: number[] = pluck(person, ['age']); // ok, number[], [ 35 ]
let persons: (string | number)[] = pluck(person, ['name', 'sex', 'age']); // [ 'Jarid', '男', 35 ]

動態聲明字符串字面量類型

  • 結合type`keyof咱們就能夠獲取跟隨interface Person`變化的字符串字面量類型
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string

映射類型 - 從舊類型中建立新類型

interface Person {
    name: string;
    age: number;
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}
type PersonPartial = Partial<Person>;
---------------------------------------
type Readonly<T> = {
    readonly [P in keyof T]: T[P];   
}
type ReadonlyPerson = Readonly<Person>;
// 至關於
type ReadonlyPerson = {
  readonly name: string;
  readonly age: number;
}

實用工具類型

  • TypeScript提供一些工具類型來幫助常見的類型轉換
  • 詳見
​Omit<T, K>​ TypeScript 3.5 //讓咱們能夠從一個對象類型中剔除某些屬性,並建立一個新的對象類型
Partial<T>,TypeScript 2.1 // 將構造類型T全部的屬性設置爲可選的
Readonly<T>,TypeScript 2.1 // 將構造類型T全部的屬性設置爲只讀的
Record<K,T>,TypeScript 2.1 // 可用來將某個類型的屬性映射到另外一個類型上
Pick<T,K>,TypeScript 2.1 // 從類型T中挑選部分屬性K來構造類型
Exclude<T,U>,TypeScript 2.8 // 從類型T中剔除全部能夠賦值給U的屬性,而後構造一個類型
Extract<T,U>,TypeScript 2.8 // 從類型T中提取全部能夠賦值給U的類型,而後構造一個類型
NonNullable<T>,TypeScript 2.8 // 從類型T中剔除null和undefined,而後構造一個類型
ReturnType<T>,TypeScript 2.8 // 由函數類型T的返回值類型構造一個類型
InstanceType<T>,TypeScript 2.8 // 由構造函數類型T的實例類型構造一個類型
Required<T>,TypeScript 2.8 // 構造一個類型,使類型T的全部屬性爲required必選
ThisType<T>,TypeScript 2.8 // 這個工具不會返回一個轉換後的類型。它作爲上下文的this類型的一個標記。注意,若想使用此類型,必須啓用--noImplicitThis

react 中使用 TypeScript

在 react 中使用 ts 的幾點原則和變化

  • 全部用到jsx語法的文件都須要以tsx後綴命名
  • 使用組件聲明時的Component<P, S>泛型參數聲明,來代替PropTypes
  • 全局變量或者自定義的window對象屬性,統一在項目根下的global.d.ts中進行聲明定義
  • 對於項目中經常使用到的接口數據對象,在types/目錄下定義好其結構化類型聲明

類組件的聲明

class App extends Component<IProps, IState> {
    static defaultProps = {
        // ...
    }
    
    readonly state = {
        // ...
    }; 
    // 小技巧:若是state很複雜不想一個個都初始化,
    // 能夠結合類型斷言初始化state爲空對象或者只包含少數必須的值的對象:
    // readonly state = {} as IState;
}

ts 斷言參考資料

須要特別強調的是,若是用到了state,除了在聲明組件時經過泛型參數傳遞其state結構,還須要在初始化state時聲明爲 readonly 這是由於咱們使用 class properties 語法對state作初始化時,會覆蓋掉Component<P, S>中對statereadonly標識。

函數式組件的聲明

// SFC: stateless function components
// v16.7起,因爲hooks的加入,函數式組件也可使用state,因此這個命名不許確。
// 新的react聲明文件裏,也定義了React.FC類型
const List: React.SFC<IProps> = props => null

TypeScript 中使用React Hook

useState

  • 大多數狀況下,useState 的類型能夠從初始化值推斷出來。但當咱們初始化值爲 null、undefined或者對象以及數組的時候,咱們須要制定useState的類型
// 能夠推斷 age 是 number類型
const [age, setAge] = useState(20);

// 初始化值爲 null 或者 undefined時,須要顯示指定 name 的類型
const [name, setName] = useState<string>();

// 初始化值爲一個對象時
interface People {
    name: string;
    age: number;
    country?: string;
}
const [owner, setOwner] = useState<People>({name: 'rrd_fe', age: 5});

// 初始化值是一個數組時
const [members, setMembers] = useState<People[]([]);

class組件都要指明props和state類型嗎?

  • 只要在組件內部使用了props和state,就須要在聲明組件時指明其類型。
  • 可是,只要咱們初始化了state,貌似即便沒有聲明state的類型,也能夠正常調用以及setState。不過,這麼作會讓組件丟失對state的訪問和類型檢查!
// bad one
class App extends Component {
    state = {
        a: 1,
        b: 2
    }
 
    componentDidMount() {
        this.state.a // ok: 1
 
        // 假如經過setState設置並不存在的c,TS沒法檢查到。
        this.setState({
            c: 3
        });
        
        this.setState(true); // ???
    }
    // ...
}
 
// React Component
class Component<P, S> {
        constructor(props: Readonly<P>);
        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;
        forceUpdate(callBack?: () => void): void;
        render(): ReactNode;
        readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;
        state: Readonly<S>;
        context: any;
        refs: {
            [key: string]: ReactInstance
        };
    }
 
 
// interface IState{
//    a: number,
//    b: number
// }

// good one
class App extends Component<{}, { a: number, b: number }> {
   
    readonly state = {
        a: 1,
        b: 2
    }
    
    //readonly state = {} as IState,斷言所有爲一個值
 
    componentDidMount() {
        this.state.a // ok: 1
 
        //正確的使用了 ts 泛型指示了 state 之後就會有正確的提示
        // error: '{ c: number }' is not assignable to parameter of type '{ a: number, b: number }'
        this.setState({
            c: 3
        });
    }
    // ...
}

使用react高階組件

什麼是 react 高階組件? 裝飾器
  • 由於react中的高階組件本質上是個高階函數的調用,因此高階組件的使用,咱們既可使用函數式方法調用,也可使用裝飾器。可是在TS中,編譯器會對裝飾器做用的值作簽名一致性檢查,而咱們在高階組件中通常都會返回新的組件,而且對被做用的組件的props進行修改(添加、刪除)等。這些會致使簽名一致性校驗失敗,TS會給出錯誤提示。這帶來兩個問題:

第一,是否還能使用裝飾器語法調用高階組件?

  • 若是這個高階組件正確聲明瞭其函數簽名,那麼應該使用函數式調用,好比 withRouter:
import { RouteComponentProps } from 'react-router-dom';
 
const App = withRouter(class extends Component<RouteComponentProps> {
    // ...
});

// 如下調用是ok的
<App />

如上例子,咱們在聲明組件時,註解了組件的props是路由的RouteComponentProps結構類型,可是咱們在調用App組件時,並不須要告知RouteComponentProps裏具備的location、history等值,這是由於withRouter這個函數自身對其作了正確的類型聲明。

第二,使用裝飾器語法或者沒有函數類型簽名的高階組件怎麼辦?

就是將高階組件注入的屬性都聲明可選(經過Partial這個映射類型),或者將其聲明到額外的injected組件實例屬性上。

import { RouteComponentProps } from 'react-router-dom';
 
// 方法一
@withRouter
class App extends Component<Partial<RouteComponentProps>> {
    public componentDidMount() {
        // 這裏就須要使用非空類型斷言了
        this.props.history!.push('/');
    }
    // ...
});
 
// 方法二
@withRouter
class App extends Component<{}> {
    get injected() {
        return this.props as RouteComponentProps
    }
 
    public componentDidMount() {
        this.injected.history.push('/');
    }
    // ...

如何正確的聲明高階組件?

  • 聲明withVisible這個高階組件時,利用泛型和類型推導,對高階組件返回的新的組件以及接收的參數組件的props都作出類型聲明。
interface IVisible {
    visible: boolean;
}
 
 //排除 IVisible
function withVisible<Self>(WrappedComponent: React.ComponentType<Self & IVisible>): 
React.ComponentType<Omit<Self, 'visible'>> {
    return class extends Component<Self> {
        render() {
            return <WrappedComponent {...this.props}  visible={true} />
        }
    }
}

拓展閱讀

參考文章
TypeScript 入門教程
使用 TypeScript 裝飾器裝飾你的代碼
優雅的在 react 中使用 TypeScript
TypeScript 中使用React Hook
TypeScript體系調研報告
相關文章
相關標籤/搜索