TypeScript進階 之 重難點梳理

點擊藍色“大前端圈”關注我喲

加個“星標”,歡迎來撩

THE LAST TIME

The last time, I have learned

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

也是給自己的查缺補漏和技術分享。

筆者文章集合詳見

  • GitHub 地址:Nealyang/personalBlog

  • 公衆號:「全棧前端精選」

前言

JavaScript 毋庸置疑是一門非常好的語言,但是其也有很多的弊端,其中不乏是作者設計之處留下的一些 “bug”。當然,瑕不掩瑜~

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

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

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

其實 Ts 的入門非常的簡單:.js to .ts; over!

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

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

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

  • Typescript 中文網

  • 深入理解 Typescript

  • TypeScript Handbook

  • TypeScript 精通指南

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

可索引類型

關於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在實戰項目中的介紹:

參考文獻

  • 未來可期的TypeScript

  • Typescript 中文文檔

  • 深入理解 Typescript

  • TypeScript 2.8下的終極React組件模式

  • 【速查手冊】TypeScript 高級類型 cheat sheet

  • 高級類型

  • TypeScript 在 React 中使用總結

點擊閱讀原文可查外鏈

如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  • 點個【在看】,或者分享轉發,讓更多的人也能看到這篇內容

  • 關注公衆號【大前端圈】,不定期分享原創&精品技術文章。

  • 公衆號內回覆:【 1 】。加入大前端圈公衆號交流羣。

---END---

推薦閱讀

張一鳴:爲什麼 BAT 挖不走我們的人才?

微信號 可以改了 !!!真事 !!

再見!杭州!再見!阿里巴巴!

2020年北京,上海擺攤夜市分佈

- 長按識別關注 -

技術,職場,產品,思維

行業觀察

                                                                          

如有收穫,點個在看,誠摯感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章