不一樣的 TypeScript 入門手冊
前言
TypeScript
是大勢所趨,也是現在大廠必備技能,作爲前端我們要與時俱進,此時不學,更待何時。
這篇文章可能不太適合 TS 純小白,需要你對 TS 有一定的瞭解,這樣的話,食用起來味道更佳。閱讀的過程中一定要有耐心,不要急於求成,建議認真看完每一個字並且邊學邊敲,這樣才能加深印象,不至於睡一覺就忘,浪費大把時間。從JavaScript
過渡到TypeScript
其實很簡單,因爲兩者語法類似,學習成本並不高,掌握這篇文章中的內容足夠日常使用。
社區裏有不少關於TypeScript
的文章,熱門的我基本都看過,大佬們寫的也確實很不錯,膜拜!但是我覺得還可以站在巨人的肩膀上再完善一下,內容上對於新手可以再友好一些,篇幅上也可以再精簡一點。另外我想通過寫作的方式進一步鞏固自己的 TS 知識,學而時習之,不亦說乎。
懷着這樣的初衷,我開始動筆,如果這篇文章能幫助到你,那是我莫大的榮幸;如果你在閱讀過程中發現錯誤或者不足之處,歡迎指正,我們共同進步。
什麼是 TypeScript
想學好一門語言,我們首先要搞清楚它是什麼。
TypeScript
是微軟開發的一個開源的編程語言,通過在JavaScript
的基礎上添加靜態類型定義構建而成。TypeScript
可以通過TypeScript編輯器
或 Babel
轉譯爲 JavaScript
代碼,可以運行在任何瀏覽器,任何操作系統。
TypeScript
起源於使用JavaScript
開發的大型項目 。由於JavaScript
語言本身的侷限性,難以勝任和維護大型項目開發,因此微軟開發了TypeScript
,使得其能夠勝任開發大型項目。
這些概念不用死記硬背,瞭解即可。
簡單總結:TypeScript
是JavaScript
的超集,具有類型系統並可以編譯爲純JavaScript
。
爲什麼要使用 TypeScript
任何一門語言的誕生和發展都是有緣由的,從某種程度上說,TypeScript
的誕生是歷史發展的必然。
Web 應用越來越複雜,導致JavaScript
代碼量激增,由於JavaScript
是動態語言,很難做到類型檢查,這直接導致很多語法問題在編譯階段無法被發現,只能在運行時暴露。(想想都頭大)
而TypeScript
是靜態類型的語言,靜態類型可以讓編譯器在編碼階段即時檢測到各類語法錯誤。使用TypeScript
進行開發,能夠避免許多潛在的 bug 。
通過是否允許隱式轉換來分類
強類型:TS
弱類型:JS
通過類型檢查的時機來分類
靜態類型:TS
動態類型:JS
複製代碼
TypeScript
給前端帶來的好處主要有以下幾點:
-
提高開發效率和代碼質量
TypeScript
不僅可以讓編輯器進行智能提示和語法錯誤檢查,還能夠實現代碼補全、接口提示、跳轉到定義和代碼重構等操作。現在你可能無法理解,等真正上手用起來,真香! -
增強了代碼的可讀性和可維護性
一般來說,理解 C# 或者 Java 會比 JavaScript 更容易,因爲他們都是強類型的,而且支持面向對象的特徵。強類型語言本身就是一個很好的說明文檔,大部分函數看類型定義就能明白大致如何使用。
JavaScript
很多庫中利用了不少高級語言的特性,開發人員可能無法很好地理解其意圖,而TypeScript
可以協助我們解決這樣的問題。 -
勝任大規模應用開發
使用
TypeScript
開發的項目,代碼結構更加清晰、一致和簡單,降低了代碼後續維護和升級的難度。
也有小部分人質疑TypeScript
,認爲沒必要去學習。在我看來,這不過是給自己的懶惰尋找藉口,當大潮退去,才知道誰在裸泳。
搭建學習環境
強烈推薦一個官方的雲編輯器 Playground[1]
使用 Playground 就無需在本地安裝環境,通過瀏覽器就可以隨時學習TypeScript
,綜合體驗也不比本地編輯器差,很適合新手使用。
TypeScript
初體驗
const a: string = 1; // Type 'number' is not assignable to type 'string'
複製代碼
上面這行代碼與普通 JS 代碼的區別是,在變量後面加了一個:
和string
,這代表只能給變量a
賦string
類型的值。我們將一個number
類型的值賦值給變量 a
,所以報錯:number 類型不可分配給 string 類型。
在 TS 中,這叫做類型註解
,類型註解
是一種爲函數或者變量添加約束的方式。
基本數據類型
八種內置類型
跟 JS 的八種內置類型一致:
const str: string = '中國萬歲';
const num: number = '666';
const bool: boolean = true;
const u: undefined = undefined;
const n: null = null;
const big: bigint = 100n;
const sym: symbol = Symbol('me');
const obj: object = {x: 1};
複製代碼
動手敲一敲,很容易理解。
注意:
null 和 undefined
默認情況下null
和undefined
是所有類型的子類型,可以把null
和undefined
賦值給其它任何類型:
// null 和 undefined 賦值給 number
let num: number = 1;
num = null;
num = undefined;
// null 和 undefined 賦值給 boolean
let bool: boolean = false;
bool = null;
bool = undefined;
// null 和 undefined 賦值給 object
let obj: object = {};
obj = null;
obj = undefined;
複製代碼
如果在tsconfig.json
裏配置了"strictNullChecks": true
,null
就只能賦值給any
、unknown
和它本身的類型(null),undefined
就只能賦值給any
、unknown
、void
和它本身的類型(undefined)。
number 和 bigint
雖然number
和bigint
都表示數字,但是這兩個類型並不兼容:
let big: bigint = 100n;
let num: number = 1;
num = big; // Type 'bigint' is not assignable to type 'number'
複製代碼
其它類型
Array
定義數組的類型有兩種方式:
1. let arr: string[] = ['劍聖', '蠻王'];
2. let array: Array<string> = ['劍姬', '銳雯'];
複製代碼
這兩種寫法都意味着,數組裏面的值只能是 string 類型,否則就會報錯:
arr.push(8); // Argument of type 'number' is not assignable to parameter of type 'string'
array = ['劍姬', '銳雯', 6]; // Type 'number' is not assignable to type 'string'
複製代碼
推薦使用第一種寫法。第二種是泛型寫法,現在你不需要掌握,後面會講到。
如果你不僅想在數組中存儲 number 類型的值,還想存儲 string 類型的值,可以這樣寫:
// 這叫聯合類型數組,先了解一下。
let arr: (number | string)[] = [1, '1'];
複製代碼
元組
什麼是元組
元組是 TS 特有的類型,跟數組類似。元組最重要的特徵是可以限制數組元素的個數和類型,看栗子:
// [string, number] 就是元組類型。數組 x 的類型必須嚴格匹配,且個數必須爲2
let x: [string, number];
x = ['Hi', 666]; // OK
x = [666, 'Hi']; // error
x = ['Hi', 666, 888]; // error
複製代碼
注意: 元組只能表示一個已知元素數量和類型的數組,越界就會報錯。如果一個數組中可能有多種類型,且數量也不確定,那就直接使用 any[]
。any 大家應該都不陌生吧,anyScript
,YYDS !
元組類型的解構賦值
元組同樣支持解構賦值:
let arr: [string, number] = ['德瑪西亞!', 666];
let [lol, action] = arr;
console.log(lol); // 德瑪西亞!
console.log(action); // 666
複製代碼
當元組中的元素較多時,這種方式就不可取了。另外需要注意,解構數組元素的個數是不能超過元組中元素個數的:
let arr: [string, number] = ['德瑪西亞!', 666];
let [lol, action, hero] = arr; // Tuple type '[string, number]' of length '2' has no element at index '2'
複製代碼
元組類型[string, number]
的長度是 2,在位置索引 2 處沒有任何元素。
元組類型的可選元素
在定義元組類型時,我們也可以通過?
來聲明元組類型的可選元素:
// 要求包含一個必須的字符串屬性,和一個可選的布爾值屬性
let arr: [string, boolean?];
arr = ['一個能打的都沒有', true];
console.log(arr); // ['一個能打的都沒有', true]
arr = ['如果暴力不是爲了殺戮'];
console.log(arr); // ['如果暴力不是爲了殺戮']
複製代碼
元組類型的剩餘元素
元組類型裏最後一個元素可以是剩餘元素,形式爲...x
,你可以把它當作 ES6 中的剩餘參數。剩餘元素代表元組類型是開放的,可以有 0 個或者多個額外的元素。例如,[number, ...string[]]
表示帶有一個number
類型的元素和任意數量string
類型的元素的元組類型。舉個栗子:
let arr: [number, ...string[]];
arr = [1, '趙信']; // ok
arr = [1, '趙信', '呂布', '亞索']; // ok
複製代碼
只讀的元組類型
我們可以爲任何元組類型加上readonly
關鍵字前綴,使其成爲只讀元組:
const arr: readonly [string, number] = ['斷劍重鑄之日', 666];
複製代碼
在使用readonly
關鍵字修飾元組類型後,任何企圖改變元組中元素的操作都會報錯:
// Cannot assign to '0' because it is a read-only property
arr[0] = '騎士歸來之時';
// Property 'push' does not exist on type 'readonly [number, string]'
arr.push(6);
複製代碼
函數
函數聲明
function sum(x: number, y: number): number {
return x + y;
}
複製代碼
上面這段代碼表示,sum
函數接收兩個number
類型的參數,並且它的返回值也是number
類型。
函數表達式
const sum = function (x: number, y: number): number {
return x + y;
}
複製代碼
箭頭函數
const sum = (x: number, y: number): number => x + y;
複製代碼
可選參數
function queryUserInfo(name: string, age?: number) {
if (age) {
return `我叫${name},${age}歲`;
}
return `我叫${name},年齡保密`;
}
queryUserInfo('王思聰', 18); // 我叫王思聰,18歲(有錢人永遠18歲!)
queryUserInfo('孫一寧'); // 我叫孫一寧,年齡保密
複製代碼
注意: 可選參數後面不允許再出現必需參數:
// 報錯:A required parameter cannot follow an optional parameter
function queryUserInfo(name: string, age?: number, sex: string) {
...
}
複製代碼
參數默認值
可以給參數一個默認值,當調用者沒有傳該參數或者傳入了undefined
時,這個默認值就生效了。
function queryUserInfo(name: string, age: number, sex: string = '不詳') {
return `姓名:${name},年齡:${age},性別:${sex}`;
}
queryUserInfo('xxx', 26); // 姓名:xxx,年齡:26,性別:不詳
複製代碼
注意: 有默認值的參數也可放置在必需參數的前面,如果想要觸發這個參數的默認值,必須要主動的傳入undefined
纔可以。
剩餘參數
function push(arr: any[], ...items: any[]) {
items.forEach(item => arr.push(item));
}
let array: any[] = [];
push(array, 1, 2, 3, '迪麗熱巴', '古力娜扎');
console.log(array); // [1, 2, 3, '迪麗熱巴', '古力娜扎']
複製代碼
函數重載
由於 JS 是動態類型語言,我們經常會使用不同類型的參數來調用同一個函數,該函數會根據不同的參數返回不同類型的調用結果:
function sum(x, y) {
return x + y;
}
sum(1, 2); // 3
sum('1', '2'); // 12 (string)
複製代碼
以上代碼可以在TS
中直接使用,但是如果開啓noImplicitAny
配置項,那麼就會提示錯誤信息:
Parameter 'x' implicitly has an 'any' type
Parameter 'y' implicitly has an 'any' type
複製代碼
該提示信息告訴我們:參數 x 和參數 y 隱式具有any
類型。爲了解決這個問題,就要給參數定義類型。
此時我們希望sum
函數的入參可以同時支持string
和number
類型,所以我們可以先定義一個聯合類型string | number
,再給這個聯合類型取個名字:
type UnionType = string | number;
複製代碼
這叫做類型別名
,先了解一下,也不難理解~
接下來我們重寫一下sum
函數:
function sum(x: UnionType, y: UnionType) {
if (typeof x === 'string' || typeof y === 'string') {
return x.toString() + y.toString();
}
return x + y;
}
複製代碼
爲sum
函數的參數顯示地設置類型之後,錯誤提示就消失了。下面我們驗證一下:
const res = sum('你', '好');
res.split('');
複製代碼
一切看起來似乎很正常,我們想當然的認爲res
變量的類型爲string
,所以我們可以正常調用字符串方法split
。但此時 TS 編譯器卻報錯了:
Property 'split' does not exist on type 'string | number'
Property 'split' does not exist on type 'number'
複製代碼
類型number
上不存在split
屬性。該如何解決?函數重載
閃亮登場。
函數重載或方法重載是使用相同名稱和不同參數數量或類型創建多個方法的一種能力,要解決上面的問題,就要爲同一個函數提供多個函數類型定義來進行函數重載,編譯器會根據這個列表去處理函數的調用。看栗子:
type UnionType = number | string;
function sum(x: number, y: number): number;
function sum(x: string, y: string): string;
function sum(x: string, y: number): string;
function sum(x: number, y: string): string;
function sum(x: UnionType, y: UnionType) {
if (typeof x === 'string' || typeof y === 'string') {
return x.toString() + y.toString();
}
return x + y;
}
const res = sum('你', '好');
res.split('');
複製代碼
上面的栗子中,我們爲sum
函數提供了各種情況的函數類型定義,從而實現函數的重載,解決了報錯問題。此處強烈建議大家動手敲一遍,根據不同函數類型定義進行驗證,加深印象。
any
在 TS 中,任何類型都可以被歸爲any
類型,any
類型是類型系統的頂級類型。
如果是一個普通類型,在賦值過程中改變類型是不被允許的:
let a: string = '伊澤瑞爾,你需要地圖嗎?';
a = 666; // Type 'number' is not assignable to type 'string'
複製代碼
但如果是any
類型,則允許被賦值爲任意類型:
let a: any = 666;
a = '哈哈哈';
a = false;
a = null;
a = undfined;
a = [];
a = {};
複製代碼
如果變量在聲明的時候,未指定其類型,那麼它會被識別爲any
類型:
let something;
something = '啦啦啦';
something = 888;
something = false;
複製代碼
等價於:
let something: any;
something = '啦啦啦';
something = 888;
something = false;
複製代碼
使用any
類型就失去了使用TS
的意義,長此以往會放鬆我們對自己的要求,儘量不要使用any
。
unknown
unknown
與any
十分相似,所有類型都可以分配給unknown
類型:
let a: unknown = 250;
a = '面對疾風吧!';
a = true;
複製代碼
unknown
與any
最大的區別是:** 任何類型的值都可以賦值給any
,同時any
類型的值也可以賦值給任何類型(never
除外)。任何類型的值都可以賦值給unknown
,但unknown
類型的值只能賦值給unknown
和any
**:
let a: unknown = 520;
let b: any = a; // ok
let a: any = 520;
let b: unknown = a // ok
let a: unknown = 520;
let b: number = a; // error
複製代碼
如果不縮小類型,就無法對unknown
類型執行任何操作:
function battle() {
return 'victory !'
}
const record: unknown = {hero: battle};
record.hero(); // error
複製代碼
這種機制起到了很強的預防性,更安全。
我們可以使用typeof
或者類型斷言
等方式來縮小未知範圍:
const a: unknown = '超神!';
a.split(''); // error
if (typeof a === 'string') {
a.split(''); // ok
}
// 類型斷言,後面會講到
(a as string).split(''); // ok
複製代碼
void
void
表示沒有任何類型,和其它類型是平等關係,不能直接賦值:
let a: void;
let b: number = a; // Type 'void' is not assignable to type 'number'
複製代碼
聲明一個void
類型的變量沒有什麼意義,一般只有在函數沒有返回值時纔會使用到它。
never
never
類型表示的是那些永不存在的值的類型。
值會永不存在的兩種情況:
-
如果一個函數執行時拋出了異常,那麼這個函數就永遠不存在返回值;
-
函數中執行無限循環的代碼,也就是死循環。
// 拋出異常
function error(msg: string): never { // ok
throw new Error(msg);
}
// 死循環
function loopForever(): never { // ok
while (true) {}
}
複製代碼
never
類型同 null
和undefined
一樣,也是任何類型的子類型,也可以賦值給任何類型。
但是沒有類型是never
的子類型或可以賦值給never
類型(除了never
本身之外),即使any
也不可以賦值給never
:
let a: never;
let b: never;
let c: any;
a = 250; // error
a = b; // ok
a = c; // error
複製代碼
在 TS 中,可以利用never
類型的特性來實現全面性檢查,看栗子:
type Type = string | number;
function inspectWithNever(param: Type) {
if (typeof param === 'string') {
// 在這裏收窄爲 string 類型
} else if (typeof param === 'number') {
// 在這裏收窄爲 number 類型
} else {
// 在這裏是 never 類型
const check: never = param;
}
}
複製代碼
在 else 分支裏,我們把既不是string
類型也不是number
類型的param
賦值給了一個顯式聲明的never
類型的變量,如果一切邏輯正確,那麼就可以編譯通過。假如有一天你的同事修改了Type
的類型:
type Type = string | number | boolean;
複製代碼
然而他忘記了同時修改inspectWithNever
方法中的控制流程,這時else
分支的param
類型會被收窄爲boolean
類型,導致無法賦值給never
類型,此時就會出現一個錯誤提示。
通過這種方法,我們可以確保inspectWithNever
方法總是窮盡了Type
的所有可能類型,使得代碼的類型絕對安全。
object、Object、{}
-
object:以下稱
小object
-
Object:以下稱
大Object
-
{}:以下稱
空對象
小object
代表的是所有非原始類型,也就是說我們不能把number
string
等原始類型賦值給小object
。在嚴格模式下,null
和undefined
類型也不能賦值給小object
。
以下類型被視爲原始類型:string、number、boolean、null、undefined、bigInt、symbol。
複製代碼
看栗子:
let obj: object;
obj = 1; // error
obj = '人在塔在!'; // error
obj = true; // error
obj = null; // error
obj = undefined; // error
obj = 100n; // error
obj = Symbol(); // error
obj = {}; // ok
複製代碼
大Object
代表所有擁有toString
hasOwnProperty
方法的類型,所以,所有原始類型和非原始類型都可以賦值給大Object
。同樣,在嚴格模式下null
和 undefined
類型也不能賦給大Object
:
let obj: Object;
obj = 1; // ok
obj = '人在塔在!'; // ok
obj = true; // ok
obj = null; // error
obj = undefined; // error
obj = 100n; // ok
obj = Symbol(); // ok
obj = {}; // ok
複製代碼
從上面的栗子中可以看出,大Object
包含原始類型,而小object
僅包含非原始類型。你可能會想,那麼大Object
是不是小object
的父類型?實際上,大Object
不僅是小object
的父類型,同時也是小object
的子類型。爲了證明這一點,我們舉個🌰:
type FatherType = object extends Object ? true : false; // true
type ChildType = Object extends object ? true : false; // true
複製代碼
注意: 儘管官網文檔上說可以使用小object
代替大Object
,但是我們任需知道它們之間的區別。
空對象
和大Object
可以互相代替,它們兩的特性一致。
Number、String、Boolean、Symbol
首字母大寫的Number
String
Boolean
Symbol
很容易與小寫的原始類型number
string
boolean
symbol
混淆,前者是相應原始類型的包裝對象,我願稱之爲對象類型。
從類型兼容性上看,對象類型兼容對應的原始類型,而反過來原始類型不兼容對應的對象類型:
let a: number = 520;
let b: Number = 250;
a = b; // Type 'Number' is not assignable to type 'number'
b = a; // ok
複製代碼
注意: 不要使用對象類型來註解值的類型,沒有任何意義。
類型推斷
先看栗子:
let str: string = '我的大刀早已飢渴難耐!'; // let str: string
let num: number = 250; // let num: number
let bool: boolean = false; // let bool: boolean
const str: string = '我的大刀早已飢渴難耐!'; // const str: string
const num: number = 250; // const num: number
const bool: boolean = false; // const bool: boolean
複製代碼
上面的栗子中,使用 let 定義變量時,我們寫明瞭類型註解,因爲值可能會改變。可是,使用 const 常量時還需要寫明類型註解,有沒有覺得有點麻煩?好在 TS 已經考慮到了這個問題。
在很多情況下,TS 會根據上下文環境自動地推斷出變量的類型,無需我們再寫明類型註解。上面的栗子可以簡化:
let str = '我的大刀早已飢渴難耐!'; // 同上
let num = 250; // 同上
let bool = false; // 同上
const str = '我的大刀早已飢渴難耐!'; // const str: "我的大刀早已飢渴難耐!"
const num = 250; // const num: 250
const bool = false; // const bool: false
複製代碼
我們把 TS 這種基於賦值表達式推斷類型的能力稱之爲類型推斷
。
在 TS 中,函數返回值、具有初始化值的變量、有默認值的函數參數的類型都可以根據上下文推斷出來。例如根據 return 語句推斷函數返回值的類型:
function sum(x: number, y: number) {
return x + y;
}
const value = sum(1, 2); // 推斷出 value 的類型是 number
//
function sum(x: number, y = 2) {
return x + y;
}
const value = sum(1); // 推斷出 value 的類型是 number
const v = sum(1, '2'); // Argument of type '"2"' is not assignable to parameter of type 'number | undefined'
複製代碼
如果定義的時候沒有賦值,不管之後有沒有賦值,都會被推斷爲any
類型:
let a; // let a: any
a = '你的劍,就是我的劍';
a = 666;
a = true;
複製代碼
類型斷言
有時候我們會遇到這樣的情況,你會比 TS 更瞭解某個值的詳細信息,你清楚的知道它的類型比現有類型更加確切:
const arr: number[] = [1, 2, 3];
const res: number = arr.find(num => num > 2); // Type 'undefined' is not assignable to type 'number'
複製代碼
上例中,res
的值一定是 3,所以它的類型應該是number
。但是 TS 的類型檢測無法做到絕對智能,在 TS 看來,res
的類型既可能是number
也可能是undefined
,所以提示錯誤信息:不能把undefined
類型分配給number
類型。
此時,類型斷言
就派上用場了。類型斷言是一種篤定的方式,它只作用於類型層面的強制類型轉換(可以理解成一種暫時的善意的謊言,不會影響運行效果),告訴編譯器應該按照我們的方式來做類型檢查。
as
使用as
語法做類型斷言:
const arr: number[] = [1, 2, 3];
const res: number = arr.find(num => num > 2) as number;
複製代碼
尖括號
另外還可以使用尖括號
語法做類型斷言:
const value: any = '我好想射點什麼!';
const valueLength: number = (<string>value).length;
複製代碼
注意: 以上兩種語法雖然沒有區別,但是尖括號
格式會與 react 中的 JSX 產生語法衝突,因此更推薦使用as
語法。
非空斷言
當類型檢查系統無法從上下文中斷定類型時,非空斷言操作符!
可以用來斷言操作對象是非null
和undefined
類型。簡單說就是,v!
將從 v 的值域中排除掉null
和undefined
:
let v: null | undefined | string;
v.toString(); // Object is possibly 'null' or 'undefined'
v!.toString(); // ok
複製代碼
type FuncType = () => number;
function fn(getNum: FuncType | undefined) {
// Object is possibly 'undefined'
// Cannot invoke an object which is possibly 'undefined'
const value1 = getNum();
const value2 = getNum!(); // ok
}
複製代碼
確定賦值斷言
TS 允許我們在實例屬性和變量聲明後面添加一個!
,用來告訴類型系統該屬性會被明確地賦值。先看一個栗子:
let x: number;
init();
console.log(x + 1); // Variable 'x' is used before being assigned
function init() {
x = 1;
}
複製代碼
上面的栗子中,提示錯誤信息:變量 x 在賦值之前被使用。我們可以用確定賦值斷言
來解決這個問題:
let x!: number;
init();
console.log(x + 1); // ok
function init() {
x = 1;
}
複製代碼
通過let x!: number
確定賦值斷言,TS 編譯器就會知道該屬性會被明確地賦值。
注意: !
不要輕易使用,如果值本身就是null
或者undefined
,使用!
僅僅是繞過了檢查,程序仍會報錯。
字面量類型
在 TS 中,字面量不僅可以表示值,還可以表示類型,即字面量類型。
目前支持三種字面量類型:字符串字面量類型、數字字面量類型、布爾值字面量類型,對應的字符串字面量、數字字面量、布爾值字面量分別擁有與其值一樣的字面量類型:
let x: '是時候表演真正的技術了!' = '是時候表演真正的技術了!';
let y: 666 = 666;
let z: false = false;
複製代碼
對此你可能會有一些疑惑。冒號後面的'是時候表演真正的技術了!
在這裏表示一個字符串字面量類型
,它其實是string
類型,準確地說是string
類型的子類型
。而string
類型不一定是字符串字面量類型
,舉個栗子:
let a: '長槍依在!' = '長槍依在!';
let b: string = '你要來幾發麼?';
a = b; // Type 'string' is not assignable to type '"長槍依在!"'
b = a; // ok
複製代碼
上面的栗子同樣適用於其它字面量類型。實際上,定義單個的字面量類型並沒有太大的用處,它真正的應用場景是把多個字面量類型組合成一個聯合類型,用來描述有明確成員的實用的集合。聯合類型後面會講到,我們先看下栗子:
type Direction = 'up' | 'down';
function move(dir: Direction) {
// ...
}
move('up'); // ok
move('left'); // Argument of type '"left"' is not assignable to parameter of type 'Direction'
複製代碼
通過使用字面量類型組合而成的聯合類型,我們可以限制函數的入參爲更具體的類型。這既提升了代碼的可讀性,也更加安全。
let 和 const
const str = '我還以爲你從來都不會選我呢'; // str: '我還以爲你從來都不會選我呢'
const num = 1; // num: 1
const bool = true; // bool: true
複製代碼
上面代碼中,我們用const
定義不可變的常量,在沒有添加類型註解的情況下,TS 推斷出常量的類型爲賦值字面量的類型。再看如下代碼:
let str = '我還以爲你從來都不會選我呢'; // str: string
let num = 1; // num: number
let bool = true; // bool: boolean
複製代碼
我們沒有給使用let
定義的變量顯式地添加類型註解,但是變量的類型自動地轉換成了賦值字面量類型的爸爸類型。
這種設計符合編程預期,所以我們可以將任何string
類型的值賦給str
,也可以將任何number
類型的值賦給num
:
str = '我還沒腳軟呢,泥腿子!';
num = 888;
bool = false;
複製代碼
我們將 TS 的字面量子類型自動轉換成爸爸類型的這種設計稱之爲字面量類型的拓寬
,下面會重點講解:
類型拓寬
所有通過let
和var
定義的變量、函數的形參、對象的非只讀屬性,如果滿足指定了初始值且未顯式添加類型註解的條件,那麼它們推斷出來的類型就是指定的初始值字面量類型拓寬後的類型,這就是字面量類型拓寬。
let str = '我用雙手成就你的夢想'; // str: string
let fn = (x = '奉均衡之命!') => x; // fn: (x?: string) => string
const a = '明智之選'; // a: '明智之選'
let b = a; // b: string
let func = (c = a) => c; // func: (c?: string) => string
複製代碼
上面的栗子一定要認真看完,結合概念去理解。加油,程序猴!
除了字面量類型拓寬之外,TS 對某些特定類型值也有類似類型拓寬的設計。例如對null
和undefined
的類型進行拓寬,通過let
var
定義的變量如果滿足未顯式添加類型註解且被賦予了null
或undefined
值,則推斷出這些變量的類型爲any
:
let x = null; // x: any
let y = undefined; // y: any
const a = null; // a: null;
const b = undefined; // b: undefined
複製代碼
再來個🌰強化下:
type ObjType = {
a: number;
b: number;
c: number;
}
type KeyType = 'a' | 'b' | 'c';
function fn(object: ObjType, key: KeyType) {
return object[key];
}
let object = {a: 123, b: 456, c: 789};
let key = 'a';
fn(object, key); // Argument of type 'string' is not assignable to parameter of type '"a" | "b" | "c"'
複製代碼
看起來似乎挺正常,可爲啥會提示錯誤信息呢?這是因爲變量key
的類型被推斷成了string
類型(類型拓寬) ,但是函數期望它的第二個參數是一個更具體的類型,所以報錯。
TS 提供了一些控制拓寬過程的方法,其中一種是使用const
,如果用const
聲明一個變量,那麼它的類型會更窄:
type ObjType = {
a: number;
b: number;
c: number;
}
type KeyType = 'a' | 'b' | 'c';
function fn(object: ObjType, key: KeyType) {
return object[key];
}
let object = {a: 123, b: 456, c: 789};
const key = 'a'; // ok
fn(object, key);
複製代碼
我們使用const
成功解決了上面的報錯問題。然而const
有時卻不起作用:
const obj = {
x: 250,
}
obj.x = 520; // ok
obj.x = '520'; // Type 'string' is not assignable to type 'number'
obj.y = 1314; // Property 'y' does not exist on type '{ x: number; }'
複製代碼
對於對象而言,TS 的拓寬算法會將其內部屬性視爲賦值給let
關鍵字聲明的變量,進而來推斷其屬性的類型。因此,obj
的類型爲{x: number}
。obj.x
的值可以是任何number
類型的值,但不允許是string
類型的,同時也不允許給obj
對象添加其它的屬性。
要解決上面的問題,我們可以使用const斷言
。它跟 var、let、const 沒有任何關係,不要混淆。
🌰:
// TS: {x: number; y: number}
const obj1 = {
x: 1,
y: 2,
}
// TS: {x: 1; y: number}
const obj2 = {
x: 1 as const,
y: 2,
}
// TS: {readonly x: 1; readonly y: 2}
const obj3 = {
x: 1,
y: 2,
} as const;
const arr1 = [1, 2, 3]; // TS: number[]
const arr2 = [1, 2, 3] as const; // TS: readonly [1, 2, 3]
複製代碼
當在某個值後面使用了const斷言
時,TS 會爲這個值推斷出最窄的類型,沒有拓寬。對於真正的常量,這通常是你想要的。
有類型拓寬,自然就有類型縮小。
類型縮小
在 TS 中,我們可以通過一些操作將變量的類型由一個較爲寬泛的集合縮小爲相對較小、較明確的集合,這就是類型縮小。
let fn = (a: any) => {
if (typeof a === 'string') {
return a;
} else if (typeof a === 'number') {
return a;
}
return null;
}
複製代碼
上面的栗子中,我們利用類型守衛將函數參數的類型從any
縮小爲明確的類型,hover 到第三行的a
提示變量類型是string
,第五行則提示變量類型是number
。
還可以利用類型守衛將聯合類型縮小爲明確的子類型:
let fn = (a: string | number) => {
if (typeof a === 'string') {
return a; // a: string
} else {
return a; // a: number
}
}
複製代碼
聯合類型
聯合類型是多種類型的集合,用來約束取值只能是某幾個值中的一個,使用|
分隔每個類型:
let a: string | number;
a = '火焰,是我最喜歡的玩具!'; // ok
a = 666; // ok
複製代碼
聯合類型經常與null
或undefined
一起使用:
const fn = (a: string | undefined) => {
...
}
fn('哈哈哈'); // ok
fn(undefined); // ok
fn(888); // Argument of type '888' is not assignable to parameter of type 'string | undefined'
複製代碼
a
的類型是聯合類型:string | undefined
,如果傳入number
類型的值就會報錯。
類型別名
類型別名就是用type
關鍵字給一個類型取個新名字,常用於聯合類型:
type Id = number | number[]; // 別名以大寫字母開頭
const delete = (id: Id) => {
...
}
複製代碼
類型別名只是給類型取一個新名字,而不是新創建一個類型。
交叉類型
交叉類型是將多個類型合併爲一個類型,這讓我們可以把現有的多種類型疊加到一起成爲一種類型。使用&
定義交叉類型:
type Value = string & number;
複製代碼
很顯然,上面定義的交叉類型是沒有任何意義的,因爲沒有任何類型可以既是string
類型又是number
類型,兩者不能同時滿足,Value 的類型是never
。
交叉類型真正的用武之地是將多個接口類型合併成一個類型,從而實現類似於繼承的效果:
interface Type1 {
name: string;
sex: string;
}
interface Type2 {
age: number;
}
type NewType = Type1 & Type2;
const person: NewType = {
name: '金克絲',
sex: '女',
age: 19,
address: '諾克薩斯', // error
}
複製代碼
上慄中,我們將Type1
和Type2
通過交叉類型合併爲NewType
,使得NewType
同時擁有了 name、sex、age 屬性。
interface
是定義接口的關鍵字,我們馬上就會學習。如果你比較迷惑,試着理解下這個栗子:
type PersonType = {name: string, sex: string} & {age: number};
const person: PersonType = {
name: '凱特琳',
sex: '女',
age: 21,
}
複製代碼
擴展一下,如果合併的多個接口類型中存在同名屬性會是什麼效果呢?
type PersonType = {name: string, sex: string} & {age: number, name: number};
複製代碼
如果同名屬性的類型不兼容,如上例中的 name 屬性,那麼合併後的類型就是string & number
,即never
。
const person: PersonType = {
name: '艾希', // Type 'string' is not assignable to type 'never'
sex: '女',
age: 18,
}
複製代碼
如果同名屬性的類型兼容,例如一個是number
,另一個是number
的子類型 (數字字面量類型),合併後 name 屬性的類型就是兩者中的子類型:
type PersonType = {name: string, age: number} & {sex: string, age: 18};
const person: PersonType = {
name: '阿木木',
sex: '男',
age: 19, // Type '19' is not assignable to type '18'
}
複製代碼
上慄中,age 屬性的類型就是數字字面量 18,所以,不能將 18 以外的任何值賦給 age 屬性。
如果同名屬性是非基本數據類型呢?
interface X {
o: {a: string},
}
interface Y {
o: {b: number},
}
interface Z {
o: {c: boolean},
}
type XYZ = X & Y & Z;
const xyz: XYZ = {
o: {
a: '啊哈哈',
b: 666,
c: true,
}
}
複製代碼
在混入多個類型時,若存在相同的成員且成員爲非基本數據類型,那麼是可以成功合併的。
接口
前面有小部分內容提到過接口,你可能雲裏霧裏,沒關係,我們現在深入學習。
什麼是接口
在 TS 中,我們使用接口(interfaces)來定義對象的類型,換句話說就是使用接口對「對象的形狀」進行描述。看栗子會更加清晰直觀:
interface Person { // 接口首字母通常大寫
name: string;
age: number;
}
const jack: Person = {
name: 'Jack',
age: 21,
}
複製代碼
上慄中,我們使用interface
關鍵字定義了一個接口Person
,接着定義了一個變量jack
,jack
的類型是Person
,這樣就約束了jack
的形狀必須和接口Person
一致。
定義的變量比接口少了一些屬性是不允許的:
interface Person {
name: string;
age: number;
}
// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'
const jack: Person = {
name: 'Jack',
}
複製代碼
多一些屬性也是不允許的:
interface Person {
name: string;
age: number;
}
const jack: Person = {
name: 'Jack',
age: 21,
sex: '男', // Object literal may only specify known properties, and 'sex' does not exist in type 'Person'
}
複製代碼
賦值時變量的形狀必須和接口的形狀保持一致。
只讀屬性
interface Person {
readonly name: string; // 只讀屬性
}
複製代碼
有時候我們希望對象中的一些字段只能在創建的時候被賦值,那麼可以用readonly
定義只讀屬性:
interface Person {
readonly id: number;
name: string;
age: number;
}
const jack: Person = {
id: 1,
name: 'Jack',
age: 21,
}
jack.id = 123; // Cannot assign to 'id' because it is a read-only property
複製代碼
上慄中,使用readonly
定義的屬性id
初始化後又被賦值,所以報錯。
注意:只讀的約束作用於第一次給對象賦值的時候,而非第一次給只讀屬性賦值的時候,舉例說明:
interface Person {
readonly id: number;
name: string;
age: number;
}
// Property 'id' is missing in type '{ name: string; age: number; }' but required in type 'Person'
const vincent: Person = {
name: 'Vincent',
age: 23,
}
vincent.id = 123; // Cannot assign to 'id' because it is a read-only property
複製代碼
上慄中,有兩處報錯:
-
對 vincent 進行賦值時,沒有給 id 賦值
-
給 vincent.id 賦值時,由於它是隻讀屬性,所以報錯了
可選屬性
interface Person {
age?: number; // 可選屬性
}
複製代碼
可選屬性是指該屬性可以不存在,當我們希望不用完全匹配一個形狀時,可以用可選屬性:
interface Person {
name: string;
age: number;
sex?: string;
}
const jack: Person = { // ok
name: 'Jack',
age: 21,
}
const ruth: Person = { // ok
name: 'Ruth',
age: 18,
sex: '女',
}
const mary: Person = {
name: 'Mary',
age: 19,
sex: '女',
address: '杭州', // error 仍然不允許添加未定義的屬性
}
複製代碼
任意屬性
有時候我們希望一個接口允許有任意的屬性,可以使用如下方式:
interface Person {
name: string;
age?: number;
[propName: string]: any; // 這叫索引簽名
}
const monroe: Person = {
name: 'Monroe',
address: '杭州',
email: 'xxxxxxxxx',
}
複製代碼
使用[propName: string]
定義了任意屬性取string
類型的值,propName
的寫法不是固定的,也可以寫成其它值,例如[key: string]
。一個接口中只能定義一個任意屬性。
注意:一旦定義了任意屬性,那麼接口中其它的確定屬性和可選屬性的類型都必須是任意屬性類型的子集。
interface Person {
name: string; // Property 'name' of type 'string' is not assignable to 'string' index type 'number'
[propName: string]: number;
}
複製代碼
上慄中,任意屬性的類型允許是 number,但確定屬性 name 的類型是 string,string 不是 number 的子集,所以報錯。
我們再看一個栗子:
interface Person {
name?: string; // Property 'name' of type 'string | undefined' is not assignable to 'string' index type 'string'
[propName: string]: string;
}
複製代碼
你有沒有感到疑惑?屬性 name 的類型是 string,任意屬性的類型是 string,符合要求,爲啥會報錯?
因爲 name 是可選屬性,name 的類型其實是 string | undefined,不是 string 的子集,所以報錯。
如果接口中有多個類型的屬性,可以在任意屬性中使用聯合類型:
interface Person {
name: string;
age?: number;
[propName: string]: string | number | undefined;
}
複製代碼
繞開額外屬性檢查的方法
鴨式辨型法
像鴨子一樣走路並且嘎嘎叫的就叫鴨子,即具有鴨子特徵的就認爲它是鴨子。所謂的鴨式辨型法,就是通過制定規則來判定對象是否實現這個接口。舉個例子:
interface Person {
name: string;
}
function getPersonInfo(personObj: Person) {
console.log(personObj.name);
}
getPersonInfo({name: '德萊文', age: 27}); // error
複製代碼
interface Person {
name: string;
}
function getPersonInfo(personObj: Person) {
console.log(personObj.name);
}
const psObj = {name: '德萊文', age: 27};
getPersonInfo(psObj); // ok
複製代碼
上面的栗子中,在參數裏寫對象就相當於直接給personObj
賦值,這個對象有嚴格的類型定義,所以不能多參或者少參。
而當我們在外面將該對象用另一個變量psObj
接收,psObj
不會經過額外屬性檢查,但是會根據類型推論爲const psObj: {name: string, age: number} = {name: '德萊文', age: 27}
。然後將psObj
再賦值給personObj
,此時根據類型的兼容性,參照「鴨式辨型法」,兩個類型因爲都具有name
屬性,所以被認定爲相同,故而可以用此方法來繞開多餘的類型檢查。
類型斷言
類型斷言的意義就等同於你在告訴程序,你很清楚自己在做什麼,此時程序就不會再進行額外的屬性檢查了:
interface Person {
name: string;
age?: number;
}
const pete: Person = {
name: 'Pete',
age: 25,
sex: '男',
} as Person; // ok
複製代碼
索引簽名
interface Person {
name: string;
age?: number;
sex: string;
[propName: string]: any;
}
const trump: Person = {
name: 'Trump',
sex: '男',
// ok
address: 'Mars',
phoneNumber: 123456,
}
複製代碼
接口與類型別名的區別
在大多數情況下,使用接口和類型別名的效果是等價的,但是在某些特定的場景下,這兩者還是存在很大區別的。
interface(接口) :
TS 的核心原則之一是對值所具有的結構進行類型檢查。而接口的作用就是爲這些類型命名並且爲我們的代碼定義數據模型(形狀)。
type(類型別名) :
類型別名是給一個類型起個新名字,起別名不會新建一個類型,它是創建了一個新名字來引用那個類型。與接口不同的是,類型別名可以作用於基本類型、聯合類型、元組以及其它任何需要你手寫的類型。
interface Person {
name: string;
age: number;
sex: string;
}
type Person = {
name: string;
age: number;
sex: string;
}
type Name = string; // 基本類型
type Sex = '男' | '女' | '不詳'; // 聯合類型
type PersonTuple = [string, number, string]; // 元組
type ComputeAge = () => number; // 函數
複製代碼
Object
interface Obj {
a: string;
b: number;
}
type Obj = {
a: string;
b: number;
}
複製代碼
Function
interface Fn {
(x: string, y: number): void;
}
type Fn = (x: string, y: number) => void;
複製代碼
接口可以定義多次,類型別名不可以
interface Obj {
x: string;
}
interface Obj {
y: number;
}
const obj: Obj = {x: '奉均衡之命!', y: 666}; // 自動合併爲單個接口
複製代碼
// error,Duplicate identifier 'Obj'
type Obj = {x: string};
type Obj = {y: number};
複製代碼
擴展
兩者的擴展方式不同,但並不互斥。接口可以擴展類型別名,反之亦然。接口的擴展就是繼承,通過extends
關鍵字來實現;類型別名的擴展就是交叉類型,通過&
來實現。
接口擴展接口
interface Obj1 {
x: string;
}
interface Obj2 extends Obj1 {
y: number;
}
const obj: Obj2 = {
x: '生與死,輪迴不止。我們生,他們死!',
y: 555,
}
複製代碼
類型別名擴展類型別名
type Obj1 = {
x: string;
}
type Obj2 = Obj1 & {
y: number;
}
const obj: Obj2 = {
x: '黑夜,就是我的舞臺',
y: 777
}
複製代碼
接口擴展類型別名
type Obj1 = {
x: string;
}
interface Obj2 extends Obj1 {
y: number;
}
const obj: Obj2 = {
x: '我的一個跟斗,能翻十萬八千里',
y: 222,
}
複製代碼
類型別名擴展接口
interface Obj1 {
x: string;
}
type Obj2 = Obj1 & {
y: number;
}
const obj: Obj2 = {
x: '只要點一下就夠了,蠢貨!',
y: 333,
}
複製代碼
泛型
初識泛型
泛型遠沒有初學者想象的那麼複雜,下面讓我們來揭開它的廬山真面目。耐心仔細的閱讀文章,學不會你拿刀砍我。
請思考這個問題:假如讓你實現一個函數,函數的參數可以是任何值,返回值就是將參數原樣返回,並且只能接收一個參數,你會怎麼做?
是不是覺得很簡單:
const fn = (arg) => arg;
複製代碼
由於可以接收任意值,所以函數的入參和返回值都應該可以是任意類型。現在我們需要給代碼增加類型聲明。此時你或許想使用 any 大法,但我勸你善良:
const fn = (arg: any) => arg;
fn('哈哈哈').length; // ok
fn('啦啦啦').toFixed(2); // ok
fn(null).toString(); // ok
複製代碼
如果使用any
的話,怎麼寫都是 OK 的,不會提示錯誤,這就失去了類型檢查的意義。上例中,我們傳入了'啦啦啦'
,類型是string
,返回值也一定是string
類型,而 string 上並沒有 toFixed 方法,這時報錯纔是我們想要的,可見使用any
不符合我們的預期。
還有一種極蠢的方法,JS 提供多少種類型,我們就寫多少種類型聲明:
type StrType = (arg: string) => string;
type NumType = (arg: number) => number;
type BoolType = (arg: boolean) => boolean;
...
複製代碼
這種寫法會導致代碼難以維護,說是屎山也不爲過~
綜上所述,最符合我們預期的是:當我們傳遞參數時,能夠根據參數的類型自動進行推導和檢查,如果傳入的是string
,但是使用了number
上的方法,就會提示錯誤。
泛型,應運而生。
function fn<T>(arg: T): T {
return arg;
}
fn('哈哈哈');
複製代碼
上慄中,我們定義了一個類型<T>
,這個T
是一個抽象類型,只有在調用的時候才能確定它的值。當我們傳入'哈哈哈'
時,T
會自動識別傳入參數的類型,進而轉換爲string
,然後再鏈式傳遞給參數類型和返回值類型,這樣一來就不用將類型寫死了。
T
代表Type
,在定義泛型時通常用作第一個類型變量名稱,T
並不是固定語法,可以用任何有效名稱代替。還有一些常見的泛型變量名:
-
K(Key):表示對象中的鍵類型
-
V(Value):表示對象中的值類型
-
E(Element):表示元素類型
泛型變量也可以定義多個:
function fn<T, U>(message: T, value: U): U {
console.log(message);
return value;
}
console.log(fn<string, number>('我喜歡你', 520));
複製代碼
工作流程:傳入參數的類型是<string, number>
,調用時會傳遞給<T, U>
,一一對應,T
就變成了string
成爲message
的類型,U
就變成了number
成爲value
的類型和返回值的類型。
fn<string, number>('我喜歡你', 520)
,這種形式是爲泛型變量顯式設定值,更常見的做法是讓編譯器自動推導這些類型。我們可以省略尖括號,使代碼更加簡潔:
function fn<T, U>(message: T, value: U): U {
console.log(message);
return value;
}
console.log(fn('我喜歡你', 520));
複製代碼
編譯器足夠智能,能夠推導出我們的參數類型,並賦值給 T 和 U,不需要開發人員去顯式地指定。
泛型約束
看下面這個栗子:
function fn<T>(arg: T): T {
console.log(arg.size); // Property 'size' does not exist on type 'T'
return arg;
}
複製代碼
我們想打印出參數的 size 屬性,但是 TS 報錯了。原因在於 T 理論上可以是任何類型,跟 any 相反,無論使用它的什麼屬性或方法都會報錯(除非這個屬性和方法是所有集合共有的)。
想要解決這個問題,我們需要對類型進行約束,限定傳給函數的參數類型應該要有 size 類型。使用extends
關鍵字可以做到這一點,簡單說就是我們先定義一個類型,然後通過extends
關鍵字讓 T 實現它即可:
interface ArgType {
size: number;
}
function fn<T extends ArgType>(arg: T): T {
console.log(arg.size);
return arg;
}
複製代碼
你可能會這麼想,直接將函數的參數限定爲 ArgType 類型不就可以了嗎?如果你這麼做,會有類型丟失的風險,具體原因就不在這裏展開討論了。
泛型工具類型
爲了方便開發者,TS 內置了一些常見的工具類型,例如:Partial、Required、Readonly、Record 等等。在具體學習工具類型之前,我們先得了解一些基礎知識。
typeof
typeof
的主要用途是在類型上下文中獲取變量或者屬性的類型:
interface Person {
name: string;
age: number;
}
const lzl: Person = {
name: '林志玲',
age: 18,
}
type LzlType = typeof lzl;
複製代碼
我不管,在我心中女神永遠 18 歲!
在上慄中,我們使用typeof
操作符獲取到lzl
變量的類型並賦值給LzlType
類型變量,之後我們就可以使用LzlType
類型了:
const zzy: LzlType = {
name: '章子怡',
age: 18,
}
複製代碼
typeof
操作符除了可以獲取對象結構的類型之外,還可以用來獲取函數的類型:
function fn(x: string): string[] {
return [x];
}
type FnType = typeof fn; // (x: string) => string[]
複製代碼
keyof
keyof
操作符可以用來獲取某種類型的所有鍵,其返回類型是聯合類型:
interface Person {
name: string;
age: number;
}
type P = keyof Person; // 'name' | 'age'
複製代碼
由於 JS 是動態類型語言,有時在靜態類型系統中捕獲某些操作的語義可能會比較麻煩,請看栗子:
function fn(obj, key) {
return obj[key];
}
複製代碼
該函數接收 obj 和 key 兩個參數,並返回對應屬性的值。對象上的不同屬性,可以具有完全不同的類型,我們甚至都不知道 obj 對象長什麼樣子。
那麼該如何定義 fn 函數的類型呢?我們來嘗試一下:
function fn(obj: object, key: string) {
return obj[key];
}
複製代碼
爲了避免調用 fn 函數時傳入錯誤的參數類型,我們爲 obj 和 key 設置了類型,分別是object
和string
。然而,並沒有這麼簡單,TS 會提示以下錯誤信息:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'
複製代碼
元素隱式地擁有any
類型,因爲string
類型不能被用於索引類型{}
。解決這個問題最暴力的方式就是使用 any 大法:
function fn(obj: object, key: string) {
return (obj as any)[key];
}
複製代碼
但很明顯這並不是一個好方案。我們來回顧一下 fn 函數的作用,該函數用於獲取某個對象中指定屬性的值,因此我們期望傳入的屬性是對象中已經存在的屬性。那麼如何限制屬性名的範圍內?靚仔keyof
閃亮登場:
function fn<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
複製代碼
上慄中,我們使用了泛型和泛型約束,還有keyof
操作符。首先定義類型 T,並使用extends
關鍵字約束T
類型必須是object
類型的子類型,然後使用keyof
操作符獲取T
類型的所有鍵,其返回值是聯合類型,最後利用extends
關鍵字約束K
類型必須是keyof T
聯合類型的子類型。這樣定義的話就能夠正確推導出指定鍵對應的類型了, 完美!這一段如果看不懂建議多看幾遍。
完整栗子:
type Person = {
name: string;
age: number;
}
const cgx: Person = {
name: '吳京',
age: 23,
}
function fn<T extends Person, K extends keyof T>(personObj: T, key: K) {
return personObj[key];
}
const uname = fn(cgx, 'name'); // const uname: string
const age = fn(cgx, 'age'); // const age: number
複製代碼
如果訪問 cgx 對象上不存在的屬性,編譯器就會報錯:
const sex = fn(cgx, 'sex'); // Argument of type '"sex"' is not assignable to parameter of type 'keyof Person'
複製代碼
in
in
用來遍歷枚舉類型
type Keys = 'x' | 'y' | 'z';
type Obj = {
[k in Keys]: string;
}
//
type Obj = {
x: string;
y: string;
z: string;
}
複製代碼
extends
有時我們不想定義的泛型過於靈活,可以通過extends
關鍵字添加泛型約束:
interface ArgType {
id: number;
}
function fn<T extends ArgType>(arg: T): T {
console.log(arg.id);
return arg;
}
複製代碼
我們對上例中的泛型進行了約束,所以它不再適用於任意類型:
fn(250); // Argument of type 'number' is not assignable to parameter of type 'ArgType'
fn({id: 250, value: '奧利給!'}); // ok
複製代碼
內置的工具類型
Partial
將類型的屬性變成可選。
定義:
type Partial<T> = {
[P in keyof T]?: T[P];
}
複製代碼
先通過keyof T
拿到T
的所有屬性名,然後使用in
進行遍歷,將值賦給P
,再通過T[P]
獲取相應屬性值的類型。?
用於將所有屬性變成可選。舉個例子:
interface Person {
name: string;
age: number;
}
type NewPerson = Partial<Person>;
const zhl: NewPerson = {
name: '鍾漢良',
}
複製代碼
這個 NewPerson 類型等同於:
interface NewPerson {
name?: string;
age?: number;
}
複製代碼
注意:Partial<T>
只支持處理第一層的屬性:
interface Person {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
type NewPerson = Partial<Person>;
const wyz: NewPerson = {
name: '吳彥祖',
address: { // Property 'city' is missing in type '{ province: string; }' but required in type '{ province: string; city: string; }'
province: '香港省',
},
}
複製代碼
可以看到,第二層以後就不會處理了。想要處理多層,我們可以自己實現:
DeepPartial
interface Person {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
}
type NewPerson = DeepPartial<Person>;
const wyz: NewPerson = {
name: '吳彥祖',
address: { // ok
province: '香港省',
},
}
複製代碼
Required
將類型的屬性變成必選。
定義:
type Required<T> = {
[K in keyof T]-?: T[K];
}
複製代碼
-?
代表移除可選特性。
interface Person {
name?: string;
age?: string;
}
type NewPerson = Required<Person>;
const zjl: NewPerson = { // Property 'age' is missing in type '{ name: string; }' but required in type 'Required<Person>'
name: '周杰倫',
}
複製代碼
Readonly
將類型的屬性變成只讀。
定義:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
}
複製代碼
栗子:
interface Person {
name: string;
age: number;
}
type NewPerson = Readonly<Person>;
const hg: NewPerson = {
name: '胡歌',
age: 18,
}
hg.age = 40; // Cannot assign to 'age' because it is a read-only property
複製代碼
Record
Record<K extends keyof any, T>
將 K 中所有屬性的值轉化爲 T 類型。
定義:
type Record<K extends keyof any, T> = {
[P in K]: T;
}
複製代碼
栗子:
interface PersonInfo {
name: string;
}
type Person = 'zxy' | 'ldh' | 'zgr';
const ny: Record<Person, PersonInfo> = {
zxy: {name: '張學友'},
ldh: {name: '劉德華'},
zgr: {name: '張國榮'},
}
複製代碼
ReturnType
用來獲取一個函數的返回值類型。
定義:
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
複製代碼
infer
用於提取函數返回值的類型。
栗子:
type Fn = (v: string) => number;
let x: ReturnType<Fn> = 888;
x = '888'; // Type 'string' is not assignable to type 'number'
複製代碼
ReturnType
提取到Fn
的返回值類型爲number
,所以變量x
只能被賦予number
類型的值。
Pick
從對象結構的類型中挑出一些指定的屬性,來構造一個新類型。
定義:
type Pick<T, U extends keyof T> = {
[P in U]: T[P];
}
複製代碼
栗子:
interface Person {
name: string;
age: number;
sex: string;
}
type NewPerson = Pick<Person, 'name' | 'sex'>;
const ldh: NewPerson = {
name: '劉德華',
sex: '男',
}
// type NewPerson = {
name: string;
sex: string;
}
複製代碼
Omit
從對象結構的類型中排除掉指定的屬性,從而構造一個新類型。
定義:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
複製代碼
栗子:
interface Person {
name: string;
age: number;
sex: string;
}
type NewPerson = Omit<Person, 'sex'>;
const ldh: NewPerson = {
name: '劉德華',
age: 18,
}
// type NewPerson = {
name: string;
age: number;
}
複製代碼
Extract
Extract<T, U>
,從 T 中提取出 U。
定義:
type Extract<T, U> = T extends U ? T : never;
複製代碼
栗子:
type A = Extract<'x' | 'y' | 'z', 'y'>; // 'y'
type B = Extract<string | number | (() => void), Function>; // () => void
複製代碼
Exclude
Exclude<T, U>
,從 T 中移除 U。
定義:
type Exclude<T, U> = T extends U ? never : T;
複製代碼
栗子:
type A = Exclude<'x' | 'y' | 'z', 'y'>; // 'x' | 'z'
type B = Exclude<string | number | (() => void), Function>; // string | number
複製代碼
NonNullable
過濾掉類型中的 null 和 undefined 類型。
定義:
type NonNullable<T> = T extends null | undefined ? never : T;
複製代碼
栗子:
type A = NonNullable<string | null | undefined>; // string
複製代碼
一些建議
儘量減少重複代碼
新手在定義接口時可能會經常出現類似的冗餘代碼:
interface Person {
name: string;
age: number;
}
interface NewPerson {
name: string;
age: number;
sex: string;
}
複製代碼
兩個接口只有一個屬性的差別,那麼如何避免重複代碼呢?我們可以使用extends
關鍵字:
interface Person {
name: string;
age: number;
}
interface NewPerson extends Person {
sex: string;
}
複製代碼
還可以使用交叉運算符&
:
type NewPerson = Person & {sex: string}
複製代碼
有時候,你想定義一個類型來匹配一個初始配置對象的 “形狀”:
const jsy = {
name: '江疏影',
age: 18,
sex: '女',
}
interface Person {
name: string;
age: number;
sex: string;
}
複製代碼
其實我們可以使用typeof
操作符來快速獲取初始配置對象的 “形狀”:
const jsy = {
name: '江疏影',
age: 18,
sex: '女',
}
type Person = typeof jsy;
複製代碼
在實際開發中,重複的類型並不總是那麼容易被發現,有時它們會被語法所掩蓋,比如多個函數擁有相同的類型簽名:
function getList(current: number, pageSize: number): Promise<Response>
function getDetailList(current: number, pageSize: number): Promise<Response>
複製代碼
對於上面的 getList 和 getDetailList 方法,我們可以提取統一的類型簽名:
type QueryList = (current: number, pageSize: number) => Promise<Response>;
const getList: QueryList = (current, pageSize) => {};
const getDetailList: QueryList = (current, pageSize) => {};
複製代碼
精準定義類型
我們首先定義一個類型:
interface Person {
name: string;
age: number;
sex: string;
birthDate: string;
income: string;
}
複製代碼
對於Person
類型,我們更希望birthDate
屬性值的格式爲YYYY-MM-DD
,income
屬性值的範圍是:low
、middle
、high
。但是在Person
接口中它們都是string
類型,所以可能會導致屬性值與預期格式不匹配:
const xdd: Person {
name: '徐鼕鼕',
age: 32,
sex: '女',
birthDate: 'February 16, 1990',
income: 'rich',
}
複製代碼
我們需要定義更精準的類型:
interface Person {
name: string;
age: number;
sex: string;
birthDate: Date;
income: 'low' | 'middle' | 'high';
}
複製代碼
重新定義 Person 接口之後,對於前面的賦值語句 TS 就會報錯:
const xdd: Person = {
name: '徐鼕鼕',
age: 32,
sex: '女',
birthDate: 'February 16, 1990', // Type 'string' is not assignable to type 'Date'
income: 'rich', // Type '"rich"' is not assignable to type '"low" | "middle" | "high"'
}
//
const xdd: Person = {
name: '徐鼕鼕',
age: 32,
sex: '女',
birthDate: new Date(1990-02-16), // ok
income: 'middle', // ok
}
複製代碼
終於見到底,還好你沒放棄~
最後
電腦屏幕前的程序猴,如果你是認認真真地看到最後,那麼相信你一定有所收穫,我只恨自己文章寫的不夠好,抱拳了!如果你只是囫圇吞棗地過了一遍,那可能收效甚微,切勿浮躁啊。功不唐捐、玉汝於成,我們一起加油!
文章中有什麼錯誤或不足之處,歡迎大家在評論區指正,第一次寫這麼長的文章,請大家多多包涵。
文章由掘金 @款冬_授權發佈,https://juejin.cn/post/7066964816107143198
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/g_zE01t45qQOmgdB3eSikA