點擊藍色“大前端圈”關注我喲
加個“星標”,歡迎來撩
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
interface
和 type
兩個關鍵字的含義和功能都非常的接近。這裏我們羅列下這兩個主要的區別:
interface
:
同名的
interface
自動聚合,也可以跟同名的class
自動聚合只能表示
object
、class
、function
類型
type
:
不僅僅能夠表示
object
、class
、function
不能重名(自然不存在同名聚合了),擴展已有的
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 關鍵字既可以來擴展已有的類型,也可以對類型進行條件限定」。在擴展已有類型時,不可以進行類型衝突的覆蓋操作。例如,基類型中鍵a
爲string
,在擴展出的類型中無法將其改爲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 是索引類型操作符」。用於獲取一個“常量”的類型,這裏的“常量”是指任何可以在編譯期確定的東西,例如const
、function
、class
等。它是從 「實際運行代碼」 通向 「類型系統」 的單行道。理論上,任何運行時的符號名想要爲類型系統所用,都要加上 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 T
是 string
或者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
泛型
泛型可能是對於前端同學來說理解起來有點困難的知識點了。通常我們說,泛型就是指定一個表示類型的變量,用它來代替某個實際的類型用於編程,而後再通過實際運行或推導的類型來對其進行替換,以達到一段使用泛型程序可以實際適應不同類型的目的。說白了,「泛型就是不預先確定的數據類型,具體的類型在使用的時候再確定的一種類型約束規範」。
泛型可以應用於 function
、interface
、type
或者 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
:K
的value
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
Pick
和 Exclude
進行組合, 實現忽略對象某些屬性功能, 源碼如下:
/**
* 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
裏面查看。
畢竟。。。搬運幾段聲明着實沒啥意思。
類型斷言
斷言這種東西還是少用。。。。不多對於初學者,估計最快熟練掌握的就是類型斷言了。畢竟 「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 2.8下的終極React組件模式
【速查手冊】TypeScript 高級類型 cheat sheet
高級類型
TypeScript 在 React 中使用總結
❝點擊閱讀原文可查外鏈
❞
完
如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
點個【在看】,或者分享轉發,讓更多的人也能看到這篇內容
關注公衆號【大前端圈】,不定期分享原創&精品技術文章。
公衆號內回覆:【 1 】。加入大前端圈公衆號交流羣。
---END---
推薦閱讀
張一鳴:爲什麼 BAT 挖不走我們的人才?
微信號 可以改了 !!!真事 !!
再見!杭州!再見!阿里巴巴!
2020年北京,上海擺攤夜市分佈
- 長按識別關注 -
技術,職場,產品,思維
行業觀察
如有收穫,點個在看,誠摯感謝