Typescript 進階 之 重難點梳理

THE LAST TIME

The last time, I have learned

【THE LAST TIME】 一直是我想寫的一個系列,旨在厚積薄發,重溫前端。html

也是給本身的查缺補漏和技術分享。前端

筆者文章集合詳見node

前言

JavaScript 毋庸置疑是一門很是好的語言,可是其也有不少的弊端,其中不乏是做者設計之處留下的一些 「bug」。固然,瑕不掩瑜~git

話說回來,JavaScript 畢竟是一門弱類型語言,與強類型語言相比,其最大的編程陋習就是可能會形成咱們類型思惟的缺失(高級詞彙,我從極客時間學到的)。而思惟方式決定了編程習慣,編程習慣奠基了工程質量,工程質量劃定了能力邊界,而學習 Typescript,最重要的就是咱們類型思惟的重塑。github

那麼其實,Typescript 在我我的理解,並不能算是一個編程語言,它只是 JavaScript 的一層殼。固然,咱們徹底能夠將它做爲一門語言去學習。網上有不少推薦 or 不推薦 Typescript 之類的文章這裏咱們不作任何討論,學與不學,用或不用,利與弊。各自拿捏~typescript

再說說 typescript(下文均用 ts 簡稱),其實對於 ts 相比你們已經不陌生了。更多關於 ts 入門文章和文檔也是已經爛大街了。此文不去翻譯或者搬運各類 api或者教程章節。只是總結羅列和解惑,筆者在學習 ts 過程當中曾疑惑的地方。道不到的地方,歡迎你們評論區積極討論。編程

其實 Ts 的入門很是的簡單:.js to .ts; over!segmentfault

可是爲何我都會寫 ts 了,卻看不懂別人的代碼呢? 這!就是入門與進階之隔。也是本文的目的所在。api

首先推薦下 ts 的編譯環境:typescriptlang.org數組

再推薦筆者收藏的幾個網站:

下面,逐個難點梳理,逐個擊破。

可索引類型

關於ts 的類型應該不用過多介紹了,多用多記 便可。介紹下關於 ts 的可索引類型。準確的說,這應該屬於接口的一類範疇。說到接口(interface),咱們都知道 ts 的核心原則之一就是對值所具備的結構進行類型檢查。 它有時被稱之爲「鴨式辯型法」或「結構性子類型」。而接口就是其中的契約。可索引類型也是接口的一種表現形式,很是實用!

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

上面例子裏,咱們定義了StringArray接口,它具備索引簽名。 這個索引簽名表示了當用number去索引StringArray時會獲得string類型的返回值。
Typescript支持兩種索引簽名:字符串和數字。 能夠同時使用兩種類型的索引,可是數字索引的返回值必須是字符串索引返回值類型的子類型。

這是由於當使用number來索引時,JavaScript會將它轉換成string而後再去索引對象。 也就是說用100(一個number)去索引等同於使用"100"(一個string)去索引,所以二者須要保持一致。

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// 錯誤:使用數值型的字符串索引,有時會獲得徹底不一樣的Animal!
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

下面的例子裏,name的類型與字符串索引類型不匹配,因此類型檢查器給出一個錯誤提示:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 能夠,length是number類型
  name: string       // 錯誤,`name`的類型與索引類型返回值的類型不匹配
}

固然,咱們也能夠將索引簽名設置爲只讀,這樣就能夠防止給索引賦值

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

interface 和 type 關鍵字

stackoverflow 上的一個高贊回答仍是很是讚的。typescript-interfaces-vs-types

interfacetype 兩個關鍵字的含義和功能都很是的接近。這裏咱們羅列下這兩個主要的區別:

interface

  • 同名的 interface 自動聚合,也能夠跟同名的 class 自動聚合
  • 只能表示 objectclassfunction 類型

type:

  • 不只僅可以表示 objectclassfunction
  • 不能重名(天然不存在同名聚合了),擴展已有的 type 須要建立新 type
  • 支持複雜的類型操做

舉例說明下上面羅列的幾點:

Objects/Functions

均可以用來表示 Object 或者 Function ,只是語法上有些不一樣而已

interface Point{
  x:number;
  y:number;
}

interface SetPoint{
  (x:number,y:number):void;
}
type Point = {
  x:number;
  y:number;
}

type SetPoint = (x:number,y:number) => void;

其餘數據類型

interface 不一樣,type 還能夠用來標書其餘的類型,好比基本數據類型、元素、並集等

type Name = string;

type PartialPointX = {x:number;};
type PartialPointY = {y:number;};

type PartialPoint = PartialPointX | PartialPointY;

type Data = [number,string,boolean];

Extend

均可以被繼承,可是語法上會有些不一樣。另外須要注意的是,interface 和 type 彼此並不互斥

interface extends interface

interface PartialPointX {x:number;};
interface Point extends PartialPointX {y:number;};

type extends type

type PartialPointX = {x:number;};
type Point = PartialPointX & {y:number;};

interface extends type

type PartialPointX = {x:number;};
interface Point extends PartialPointX {y:number;};

type extends interface

interface ParticalPointX = {x:number;};

type Point = ParticalPointX & {y:number};

implements

一個類,能夠以徹底相同的形式去實現interface 或者 type。可是,類和接口都被視爲靜態藍圖(static blueprints),所以,他們不能實現/繼承 聯合類型的 type

interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x: 1;
  y: 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x: 1;
  y: 2;
}

type PartialPoint = { x: number; } | { y: number; };

// FIXME: can not implement a union type
class SomePartialPoint implements PartialPoint {
  x: 1;
  y: 2;
}

聲明合併

type 不一樣,interface 能夠被重複定義,而且會被自動聚合

interface Point {x:number;};
interface Point {y:number;};

const point:Pint = {x:1,y:2};

only interface can

在實際開發中,有的時候也會遇到 interface 可以表達,可是 type 作不到的狀況:給函數掛載屬性

interface FuncWithAttachment {
  (param: string): boolean;
  someProperty: number;
}

const testFunc: FuncWithAttachment = function(param: string) {
  return param.indexOf("Neal") > -1;
};
const result = testFunc("Nealyang"); // 有類型提醒
testFunc.someProperty = 4;

& 和 | 操做符

這裏咱們須要區分,|& 並不是位運算符。咱們能夠理解爲&表示必須同時知足全部的契約。|表示能夠只知足一個契約。

interface IA{
  a:string;
  b:string;
}

type TB{
  b:number;
  c:number [];
}

type TC = TA | TB;// TC 的 key,包含 ab 或者 bc 便可,固然,包含 bac 也能夠
type TD = TA & TB;// TD 的 能夠,必須包含 abc

交叉類型

交叉類型,咱們能夠理解爲合併。其實就是將多個類型合併爲一個類型

Man & WoMan
  • 同時是 Man 和 Woman
  • 同時擁有 Man 和 Woman 這兩種類型的成員
interface ObjectConstructor{
  assign<T,U>(target:T,source:U):T & U;
}

以上是 ts 的源碼實現,下面咱們再看一個咱們平常使用中的例子

interface A{
  name:string;
  age:number;
  sayName:(name:string)=>void
}

interface B{
  name:string;
  gender:string;
  sayGender:(gender:string)=>void
}

let a:A&B;

// 這是合法的
a.age
a.sayGender

注意:16446

T & never = never

extends

extends 即爲擴展、繼承。在 ts 中,extends 關鍵字既能夠來擴展已有的類型,也能夠對類型進行條件限定。在擴展已有類型時,不能夠進行類型衝突的覆蓋操做。例如,基類型中鍵astring,在擴展出的類型中沒法將其改成number

type num = {
  num:number;
}

interface IStrNum extends num {
  str:string;
}

// 與上面等價
type TStrNum = A & {
  str:string;
}

在 ts 中,咱們還能夠經過條件類型進行一些三目操做:T extends U ? X : Y

type IsEqualType<A , B> = A extends B ? (B extends A ? true : false) : false;

type NumberEqualsToString = IsEqualType<number,string>; // false
type NumberEqualsToNumber = IsEqualType<number,number>; // true

keyof

keyof 是索引類型操做符。用於獲取一個「常量」的類型,這裏的「常量」是指任何能夠在編譯期肯定的東西,例如constfunctionclass等。它是從 實際運行代碼 通向 類型系統 的單行道。理論上,任何運行時的符號名想要爲類型系統所用,都要加上 typeof

在使用class時,class名錶示實例類型,typeof class表示 class自己類型。是的,這個關鍵字和 js 的 typeof 關鍵字重名了 。

假設 T 是一個類型,那麼 keyof T 產生的類型就是 T 的屬性名稱字符串字面量類型構成的聯合類型(聯合類型比較簡單,和交叉類型對立類似,這裏就不作介紹了)。

注意!上述的 T 是數據類型,並不是數據自己

interface IQZQD{
    cnName:string;
    age:number;
    author:string;
}
type ant = keyof IQZQD;

vscode 上,咱們能夠看到 ts 推斷出來的 ant

注意,若是 T 是帶有字符串索引的類型,那麼keyof Tstring或者number類型。

索引簽名參數類型必須爲 "string" 或 "number"

interface Map<T> {
  [key: string]: T;
}

//T[U]是索引訪問操做符;U是一個屬性名稱。
let keys: keyof Map<number>; //string | number
let value: Map<number>['antzone'];//number

泛型

泛型多是對於前端同窗來講理解起來有點困難的知識點了。一般咱們說,泛型就是指定一個表示類型的變量,用它來代替某個實際的類型用於編程,然後再經過實際運行或推導的類型來對其進行替換,以達到一段使用泛型程序能夠實際適應不一樣類型的目的。說白了,泛型就是不預先肯定的數據類型,具體的類型在使用的時候再肯定的一種類型約束規範

泛型能夠應用於 functioninterfacetype 或者 class 中。可是注意,泛型不能應用於類的靜態成員

幾個簡單的例子,先感覺下泛型

function log<T>(value: T): T {
    console.log(value);
    return value;
}

// 兩種調用方式
log<string[]>(['a', ',b', 'c'])
log(['a', ',b', 'c'])
log('Nealyang')
  • 泛型類型、泛型接口
type Log = <T>(value: T) => T
let myLog: Log = log

interface Log<T> {
    (value: T): T
}
let myLog: Log<number> = log // 泛型約束了整個接口,實現的時候必須指定類型。若是不指定類型,就在定義的以後指定一個默認的類型
myLog(1)

咱們也能夠把泛型變量理解爲函數的參數,只不過是另外一個維度的參數,是表明類型而不是表明值的參數。

class Log<T> { // 泛型不能應用於類的靜態成員
    run(value: T) {
        console.log(value)
        return value
    }
}

let log1 = new Log<number>() //實例化的時候能夠顯示的傳入泛型的類型
log1.run(1)
let log2 = new Log()
log2.run({ a: 1 }) //也能夠不傳入類型參數,當不指定的時候,value 的值就能夠是任意的值

類型約束,需預約義一個接口

interface Length {
    length: number
}
function logAdvance<T extends Length>(value: T): T {
    console.log(value, value.length);
    return value;
}

// 輸入的參數無論是什麼類型,都必須具備 length 屬性
logAdvance([1])
logAdvance('123')
logAdvance({ length: 3 })

泛型的好處:

  • 函數和類能夠輕鬆的支持多種類型,加強程序的擴展性
  • 沒必要寫多條函數重載,冗長的聯合類型聲明,加強代碼的可讀性
  • 靈活控制類型之間的約束

泛型,在 ts 內部也都是很是經常使用的,尤爲是對於容器類很是經常使用。而對於咱們,仍是要多使用,多思考的,這樣纔會有更加深入的體會。同時也對塑造咱們類型思惟很是的有幫助。

小試牛刀

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}

let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name', 'name', 'name']); //["Jarid", "Jarid", "Jarid"]

所謂的小試牛刀,就是結合上面咱們說的那幾個點,分析下pluck方法的意思

  • <T, K extends keyof T>約束了這是一個泛型函數

    • keyof T 就是取 T 中的全部的常量 key(這個例子的調用中),即爲: "name" | "age"
    • K extends keyof Person 即爲 K 是 "name" or "age"
  • 結合以上泛型解釋,再看形參

    • K[] 即爲 只能包含"name" or "age"的數組
  • 再看返回值

    • T[K][] 後面的[]是數組的意思。而 T[K]就是去對象的 T 下的key: Kvalue

infer

infer 關鍵字最先出如今 PR 裏面,表示在 extends 條件語句中待推斷的類型變量

是在 ts2.8 引入的,在條件判斷語句中,該關鍵字用於替換手動獲取類型

type PramType<T> = T extends (param : infer p) => any ? p : T;

在上面的條件語句中,infer P 表示待推斷的函數參數,若是T能賦值給(param : infer p) => any,則結果是(param: infer P) => any類型中的參數 P,不然爲T.

interface INealyang{
  name:'Nealyang';
  age:'25';
}

type Func = (user:INealyang) => void;

type Param = ParamType<Func>; // Param = INealyang
type Test = ParamType<string>; // string

工具泛型

所謂的工具泛型,其實就是泛型的一些語法糖的實現。徹底也是能夠本身的寫的。咱們也能夠在lib.d.ts中找到他們的定義

Partial

Partial的做用就是將傳入的屬性變爲可選。

因爲 keyof 關鍵字已經介紹了。其實就是能夠用來取得一個對象接口的全部 key 值。在介紹 Partial 以前,咱們再介紹下 in 操做符:

type Keys = "a" | "b"
type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any }

而後再看 Partial 的實現:

type Partial<T> = { [P in keyof T]?: T[P] };

翻譯一下就是keyof T 拿到 T 全部屬性名, 而後 in 進行遍歷, 將值賦給 P, 最後 T[P] 取得相應屬性的值,而後配合?:改成可選。

Required

Required 的做用是將傳入的屬性變爲必選項, 源碼以下

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

Readonly

將傳入的屬性變爲只讀選項, 源碼以下

type Readonly<T> = { readonly [P in keyof T]: T[P] };

Record

該類型能夠將 K 中全部的屬性的值轉化爲 T 類型,源碼實現以下:

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

能夠根據 K 中的全部可能值來設置 key,以及 value 的類型,舉個例子:

type T11 = Record<'a' | 'b' | 'c', Person>; // -> { a: Person; b: Person; c: Person; }

Pick

T 中取出 一系列 K 的屬性

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Exclude

Exclude 將某個類型中屬於另外一個的類型移除掉。

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

以上語句的意思就是 若是 T 能賦值給 U 類型的話,那麼就會返回 never 類型,不然返回 T,最終結果是將 T 中的某些屬於 U 的類型移除掉

舉個栗子:

type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'b' | 'd'

能夠看到 T'a' | 'b' | 'c' | 'd' ,而後 U'a' | 'c' | 'f' ,返回的新類型就能夠將 U 中的類型給移除掉,也就是 'b' | 'd' 了。

Extract

Extract 的做用是提取出 T 包含在 U 中的元素,換種更加貼近語義的說法就是從 T 中提取出 U,源碼以下:

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

Demo:

type T01 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'a' | 'c'

Omit

PickExclude 進行組合, 實現忽略對象某些屬性功能, 源碼以下:

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Demo:

// 使用
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }

更多工具泛型

其實經常使用的工具泛型大概就是我上面介紹的幾種。更多的工具泛型,能夠經過查看 lib.es5.d.ts裏面查看。

畢竟。。。搬運幾段聲明着實沒啥意思。

羅列 api 的寫着也怪無聊的...

類型斷言

斷言這種東西仍是少用。。。。很少對於初學者,估計最快熟練掌握的就是類型斷言了。畢竟 any 大法好

Typescript 容許咱們覆蓋它的推斷(畢竟代碼使咱們本身寫的),而後根據咱們自定義的類型去分析它。這種機制,咱們稱之爲 類型斷言

const nealyang = {};
nealyang.enName = 'Nealyang'; // Error: 'enName' 屬性不存在於 ‘{}’
nealyang.cnName = '一凨'; // Error: 'cnName' 屬性不存在於 '{}'
interface INealyang = {
  enName:string;
  cnName:string;
}

const nealyang = {} as INealyang; // const nealyang = <INealyang>{};
nealyang.enName = 'Nealyang';
nealyang.cnName = '一凨';

類型斷言比較簡單,其實就是「糾正」ts對類型的判斷,固然,是否是糾正就看你本身的了。

須要注意一下兩點便可:

  • 推薦類型斷言的預發使用 as關鍵字,而不是<> ,防止歧義
  • 類型斷言並不是類型轉換,類型斷言發生在編譯階段。類型轉換髮生在運行時

函數重載

在我剛開始使用 ts 的時候,我一直困惑。。。爲何會有函數重載這麼雞肋的寫法,可選參數它不香麼?

慢慢你品

函數重載的基本語法:

declare function test(a: number): number;
declare function test(a: string): string;

const resS = test('Hello World');  // resS 被推斷出類型爲 string;
const resN = test(1234);           // resN 被推斷出類型爲 number;

這裏咱們申明瞭兩次?!爲何我不能判斷類型或者可選參數呢?後來我遇到這麼一個場景,

interface User {
  name: string;
  age: number;
}

declare function test(para: User | number, flag?: boolean): number;

在這個 test 函數裏,咱們的本意多是當傳入參數 para 是 User 時,不傳 flag,當傳入 para 是 number 時,傳入 flag。TypeScript 並不知道這些,當你傳入 para 爲 User 時,flag 一樣容許你傳入:

const user = {
  name: 'Jack',
  age: 666
}

// 沒有報錯,可是與想法違背
const res = test(user, false);

使用函數重載能幫助咱們實現:

interface User {
  name: string;
  age: number;
}

declare function test(para: User): number;
declare function test(para: number, flag: boolean): number;

const user = {
  name: 'Jack',
  age: 666
};

// bingo
// Error: 參數不匹配
const res = test(user, false);

Ts 的一些實戰

我以前在公衆號裏面發表過兩篇關於TS在實戰項目中的介紹:

參考文獻

學習交流

  • 關注公衆號【全棧前端精選】,每日獲取好文推薦
  • 添加微信號:is_Nealyang(備註來源) ,入羣交流
公衆號【全棧前端精選】 我的微信【is_Nealyang】
相關文章
相關標籤/搜索