類型約束 extends#
語法:T extends K
,這裡的 extends
不是類、介面的繼承,而是對於類型的判斷和約束,意思是判斷 T
能否賦值給 K
。
判斷 T
是否可以賦值給 U
,可以的話返回 T
,否則返回 never
:
type Exclude<T, U> = T extends U ? T : never
類型映射 in#
遍歷指定介面的 key 或者是遍歷聯合類型:
interface Person {
name: string
age: number
gender: number
}
// 將 T 的所有屬性轉換為只讀類型
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// type ReadonlyPerson = {
// readonly name: string
// readonly age: number
// readonly gender: number
// }
type ReadonlyPerson = Readonly<Person>
類型謂詞 is#
TypeScript 裡有類型保護機制。要定義一個類型保護,我們只要簡單地定義一個函數,它的返回值是一個類型謂詞:
function isString(test: any): test is string {
return typeof test === 'string'
}
上述寫法與寫一個返回值為 boolean 值函數的區別在哪裡呢?
function isString(test: any): boolean {
return typeof test === 'string'
}
當使用 is 類型保護:
function isString(test: any): test is string {
return typeof test === 'string'
}
function example(foo: any) {
if (isString(foo)) {
console.log('it is a string' + foo)
console.log(foo.length) // string function
// 如下代碼編譯時會出錯,運行時也會出錯,因為 foo 是 string 不存在 toExponential 方法
console.log(foo.toExponential(2))
}
// 編譯不會出錯,但是運行時出錯
console.log(foo.toExponential(2))
}
example('hello world')
當返回值為 boolean:
function isString(test: any): boolean {
return typeof test === 'string'
}
function example(foo: any) {
if (isString(foo)) {
console.log('it is a string' + foo)
console.log(foo.length) // string function
// foo 為 any,編譯正常。但是運行時會出錯,因為 foo 是 string 不存在 toExponential 方法
console.log(foo.toExponential(2))
}
}
example('hello world')
總結:
- 在使用類型保護時,TS 會進一步縮小變量的類型。例子中,將類型從 any 縮小至了 string;
- 類型保護的作用域僅僅在 if 後的塊級作用域中生效。
實戰:
function isAxiosError(error: any): error is AxiosError {
return error.isAxiosError
}
if (isAxiosError(err)) {
code = `Axios-${err.code}`
}
待推斷類型 infer#
可以用 infer P
來標記一個泛型,表示這個泛型是一個待推斷的類型,並且可以直接使用。
獲取函數參數類型:
type ParamType<T> = T extends (param: infer P) => any ? P : T
type FunctionType = (value: number) => boolean
type Param = ParamType<FunctionType> // type Param = number
type OtherParam = ParamType<symbol> // type Param = symbol
判斷 T
是否能賦值給 (param: infer P) => any
,並且將參數推斷為泛型 P
,如果可以賦值,則返回參數類型 P
,否則返回傳入的類型。
獲取函數返回類型:
type ReturnValueType<T> = T extends (param: any) => infer U ? U : T
type FunctionType = (value: number) => boolean
type Return = ReturnValueType<FunctionType> // type Return = boolean
type OtherReturn = ReturnValueType<number> // type OtherReturn = number
判斷 T
是否能賦值給 (param: any) => infer U
,並且將返回值類型推斷為泛型 U
,如果可以賦值,則返回返回值類型 P
,否則返回傳入的類型。
原始類型保護 typeof#
語法:typeof v === 'typename'
或 typeof v !== 'typename'
,用來判斷數據的類型是否是某個原始類型(number
、string
、boolean
、symbol
)並進行類型保護。
typename 必須是 number
、string
、boolean
、symbol
。 但是 TypeScript 並不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。
示例:print 函數會根據參數類型打印不同的結果,那如何判斷參數是 string
還是 number
呢?
function print(value: number | string) {
// 如果是 string 類型
// console.log(value.split('').join(', '))
// 如果是 number 類型
// console.log(value.toFixed(2))
}
兩種常用的判斷方式:
- 根據是否包含
split
屬性判斷是string
類型,是否包含toFixed
方法判斷是number
類型。弊端:不論是判斷還是調用都要進行類型轉換。 - 使用類型謂詞
is
。弊端:每次都要去寫一個工具函數,太麻煩。
故這裡可以使用 typeof
:
function print(value: number | string) {
if (typeof value === 'string') {
console.log(value.split('').join(', '))
} else {
console.log(value.toFixed(2))
}
}
使用 typeof
進行類型判斷後,TypeScript 會將變量縮減為那個具體的類型,只要這個類型與變量的原始類型是兼容的。
類型保護 instanceof#
與 typeof
類似,不過作用方式不同,instanceof
類型保護是通過構造函數來細化類型的一種方式。
instanceof
的右側要求是一個構造函數,TypeScript 將細化為:
- 此構造函數的
prototype
屬性的類型,如果它的類型不為any
的話 - 構造簽名所返回的類型的聯合
class Bird {
fly() {
console.log('Bird flying')
}
layEggs() {
console.log('Bird layEggs')
}
}
class Fish {
swim() {
console.log('Fish swimming')
}
layEggs() {
console.log('Fish layEggs')
}
}
const bird = new Bird()
const fish = new Fish()
function start(pet: Bird | Fish) {
// 調用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs()
if (pet instanceof Bird) {
pet.fly()
} else {
pet.swim()
}
// 等同於下面
// if ((pet as Bird).fly) {
// (pet as Bird).fly();
// } else if ((pet as Fish).swim) {
// (pet as Fish).swim();
// }
}
索引類型查詢操作符 keyof#
語法:keyof T
,對於任何類型 T
, keyof T
的結果為 T
上已知的公共屬性名的聯合。
keyof
與 Object.keys 略有相似,只不過 keyof 取 interface 的鍵。
interface Point {
x: number
y: number
}
// type keys = "x" | "y"
type keys = keyof Point
假設有一個 object 如下所示,我們需要使用 typescript 實現一個 get 函數來獲取它的屬性值:
function get(o: object, name: string) {
return o[name]
}
我們剛開始可能會這麼寫,不過它有很多缺點:
- 無法確認返回類型:這將損失 ts 最大的類型校驗功能;
- 無法對 key 做約束:可能會犯拼寫錯誤的問題。
這時可以使用 keyof 來加強 get 函數的類型功能:
function get<T extends object, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}
需要注意,keyof
只能返回類型上已知的公共屬性名:
class Animal {
type: string
weight: number
private speed: number
}
type AnimalProps = keyof Animal // 'type' | 'weight'
當需要獲取對象的某個屬性值,但是不確定是哪些屬性,這個時候可以使用 extends
配合 typeof
對屬性名進行限制,限制傳入的參數只能是對象的屬性名:
const person = {
name: 'Jack',
age: 20,
}
function getPersonValue<T extends keyof typeof person>(fieldName: keyof typeof person) {
return person[fieldName]
}
const nameValue = getPersonValue('name')
const ageValue = getPersonValue('age')