TypeScript 高級(一)

類型推斷

基于賦值表達式推斷類型的能力稱之為“類型推斷”。

在 TypeScript 中,具有初始化值的變量、有默認值的函數(shù)參數(shù)、函數(shù)返回值的類型都可以根據(jù)上下文推斷出來。比如能根據(jù) return 語句推斷函數(shù)返回的類型。

let str = 'hello' // string
let num = 2 // number
const bar = 'bar' //  'bar' 類型

// 推斷返回值類型為 number
function add(a: number, b: number) {
  return a + b
}

上下文推斷

通過變量所在的上下文環(huán)境推斷變量的類型。

type Add = (a: number, b: number) => number
const add: Add = (a, b) => {
  return a + b
}
// 推斷出 a, b, 函數(shù)返回值都是 number 類型

字面量類型

在 TypeScript 中,字面量不僅可以表示值,還可以表示類型,即所謂的字面量類型。

目前,TypeScript 支持 3 種字面量類型:字符串字面量類型、數(shù)字字面量類型、布爾字面量類型,對應的字符串字面量、數(shù)字字面量、布爾字面量分別擁有與其值一樣的字面量類型,示例如下:

let num: 3 = 3
let bool: true = true
let str: 'xiaomi' = 'xiaomi'

字面量類型是集合類型的子類型,它是集合類型的一種更具體的表達。

let specifiedStr: 'this is string' = 'this is string'
let str: string = 'any string'
specifiedStr = str // ts(2322) 類型 '"string"' 不能賦值給類型 'this is string'
str = specifiedStr // ok

實際上,定義單個的字面量類型并沒有太大的用處,真正的應用場景是可以把多個字面量類型組合成一個聯(lián)合類型,用于描述擁有明確成員的集合。

type Direction = 'up' | 'down'

通過使用字面量類型組合的聯(lián)合類型,可以限制函數(shù)的參數(shù)為指定的字面量類型集合,然后編譯器會檢查參數(shù)是否是指定的字面量類型集合里的成員。因此,相較于使用 string 類型,使用字面量類型(組合的聯(lián)合類型)可以將函數(shù)的參數(shù)限定為更具體的類型。不僅提升程序的可讀性,還保證了函數(shù)的參數(shù)類型。

類型拓寬

Literal Widening(字面量類型拓寬)

所有通過 let 或 var 定義的變量、函數(shù)的形參、對象的非只讀屬性,如果滿足指定了初始值且未顯式添加類型注解的條件,那么它們推斷出來的類型就是指定的初始值字面量類型拓寬后的類型,就是字面量類型拓寬。

let str = 'this is string' // 類型是 string
let strFun = (str = 'this is string') => str // 類型是 (str?: string) => string;
const specifiedStr = 'this is string' // 類型是 'this is string'
let str2 = specifiedStr // 類型是 'string'
let strFun2 = (str = specifiedStr) => str // 類型是 (str?: string) => string;

基于字面量類型拓寬的條件,可以通過如下所示代碼添加顯示類型注解控制類型拓寬行為。

const specifiedStr: 'this is string' = 'this is string' // 類型是 '"this is string"'
let str2 = specifiedStr // 即便使用 let 定義,類型是 'this is string'

除了字面量類型拓寬之外,TypeScript 對某些特定類型值也有類似 "Type Widening" (類型拓寬)的設計。

比如對 null 和 undefined 的類型進行拓寬,通過 let、var 定義的變量如果滿足未顯式聲明類型注解且被賦予了 null 或 undefined 值,則推斷出這些變量的類型是 any。

let x = null // 類型拓寬成 any
let y = undefined // 類型拓寬成 any

Type Narrowing (類型縮小)

在 TypeScript 中,可以通過某些操作將變量的類型由一個較為寬泛的集合縮小到相對較小、較明確的集合,這就是類型縮小。

比如,使用類型守衛(wèi)將形參的類型從 any 縮小到明確的類型:

const func = (foo: any) => {
  if (typeof foo === 'string') {
    return foo // string
  } else if (typeof foo === 'number') {
    return foo // number
  }
  return null
}

還可以使用類型守衛(wèi)將聯(lián)合類型縮小到明確的子類型:

const func = (foo: string | number) => {
  if (typeof foo === 'string') {
    return foo // string
  } else {
    return foo // number
  }
}

也可以通過字面量類型等值判斷(===)或其他控制流語句(包括但不限于 if、三目運算符、switch 分支)將聯(lián)合類型收斂為更具體的類型,如下代碼所示:

type Color = 'green' | 'red' | 'gray'

const getStatus = (status: Color) => {
  if (status === 'green') return status
  if (status === 'red') return status
  if (status === 'gray') return status
}

大致總結:

let 聲明的簡單類型字面量會拓寬類型,const 聲明的簡單類型字面量會收窄,const 聲明的對象變量會自動推斷對應的類型,可以用 as const 收窄,讓每一個 key 都是固定類型(只讀【readonly】的值)

類型謂詞(is)

TypeScript 中,函數(shù)還支持另外一種特殊的類型描述:

function isString(s): s is string {
  // 類型謂詞
  return typeof s === 'string'
}

function isNumber(n: number) {
  return typeof n === 'number'
}

function operator(x: unknown) {
  if (isString(x)) {
    // ok x 類型縮小為 string
  }

  if (isNumber(x)) {
    // ts(2345) unknown 不能賦值給 number
  }
}

分布式有條件類型

有條件的類型會以一個條件表達式進行類型關系檢測,從而在兩種類型中選擇其一,使用關鍵字 extends 配合三目運算符:

T extends U ? X : Y

如果 extends 前面是簡單的條件判斷,則直接判斷前面的類型是否可分配給后面的類型。

若 extends 前面的類型是泛型,且泛型傳入的是聯(lián)合類型時,則會依次判斷該聯(lián)合類型的所有子類型是否可分配給 extends 后面的類型(是一個分發(fā)的過程)。

即 T extends U ? X : Y,若 T 的類型為 A | B | C,則會被解析為(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)。

// type A1 = 1
type A1 = 'x' extends 'x' ? 1 : 2

// type A2 = 2
type A2 = 'x' | 'y' extends 'x' ? 1 : 2

// type A3 = 1 | 2
type P<T> = T extends 'x' ? 1 : 2
type A3 = P<'x' | 'y'>

type StrOrNum<T> = T extends string ? T : number
type Str = StrOrNum<string>

// null undefined
type Str<T> = T extends null | undefined ? T : never
type Strs = StrNull<string> // never

type StrNull<T> = T extends string | null | undefined ? T : never
type Strs = StrNull<string | number> // string | number

如果用于簡單的條件判斷,則是直接判斷前面的類型是否可分配給后面的類型

const 斷言

interface Info {
  readonly name: string
  readonly age: number
}

const info: Info = {
  name: 'hack',
  age: 23
}
// info.age = 4 // Error: ts(2540) age 為只讀屬性

// 等價于

// 使用 const 斷言
const obj = {
  name: 'jack',
  age: 34
} as const
obj.age = 5 // Error: ts(2540) age 為只讀屬性

還可以將數(shù)組轉成 readonly 元組

const arr = ['str', 12] as const
arr[1] = 55 // 無法為“1”賦值,因為它是只讀屬性。ts(2540)

typeof 操作符

typeof 操作符用來獲取一個變量或對象的類型。也就是說 TS 對 typeof 操作符做了擴展:

type Person = {
  name: string
  age: number
}

const person: Person = {
  name: 'xiaomi',
  age: 18
}

type Human = typeof person
// type Human = {
//   name: string
//   age: number
// }

keyof

對于任何類型 T,keyof T 的結果為該類型上所有公共屬性名的字符串或數(shù)字的組成的聯(lián)合類型。

type Point = { x: number; 1: number }
type Keys = keyof Point
/**
  Keys 的類型為
  type Keys = 'x' | 1
 */

如果該類型具有 string 或 number 索引簽名,keyof 則將返回這些類型:

type Arrayish = { [n: number]: unknown }
type A = keyof Arrayish
// type A = number

type Mapish = { [k: string]: boolean }
type M = keyof Mapish
// type M = string | number

M 類型是 string | number,是因為 JavaScript 對象鍵始終會強制轉換為字符串,因此 obj[0] === obj["0"]

根據(jù) TS 中對象的值或鍵創(chuàng)建聯(lián)合類型

從對象的值或鍵創(chuàng)建聯(lián)合類型:

  1. as const 將對象的屬性設置為readonly
  2. 用 `keyof typeof1 獲取對象中鍵的類型
  3. 使用鍵來獲取值的并集
// ??? const obj: {readonly name: "Bobby Hadz"; readonly country: "Chile";}
const obj = {
  name: 'Bobby Hadz',
  country: 'Chile',
} as const;

// ??? type UValues = "Bobby Hadz" | "Chile"
type UValues = (typeof obj)[keyof typeof obj];

// ??? type UKeys = "name" | "country"
type UKeys = keyof typeof obj;

  1. 使用as const聲明對象是一個只讀的
  2. 使用typeof 獲取對象的類型
  3. 使用keyof typeof 獲取對象key的聯(lián)合

T[K] 索引訪問操作符

T[keyof K]的方式,獲取到的是 T 中的 key 且同時存在于 K 時的類型組成的聯(lián)合類型。

如果[]中的 key 有不存在 T 中,則是 any;因為 TS 也不知道該 key 最終是什么類型,所以是 any;且也會報錯。

interface Eg {
  name: string
  readonly age: number
}
// string
type V1 = Eg['name']
// string | number
type V2 = Eg['name' | 'age']
// any
type V3 = Eg['name' | 'age2222'] // Error
// string | number
type V4 = Eg[keyof Eg1]

映射類型

typeScript 提供了從舊類型中創(chuàng)建新類型的一種方式 — 映射類型。 在映射類型里,新類型以相同的形式去轉換舊類型里每個屬性。

需要使用關鍵字in

interface Ra {
  name: string
  age: number
}

type ReadonlyRa<T> = {
  readonly [K in keyof T]: T[K]
}

const ra: ReadonlyRa<Rabbit> = {
  name: 'jack',
  age: 3
}

! 非空斷言操作符

x! 將從 x 值域中排除 nullundefined。比如給某一個元素綁定點擊事件

// const ele: HTMLElement = document.getElementById('#app')

// 因為ele 可能的值為 null 或 DOM節(jié)點。但是我們確定 ele 一定不為null,則通過添加 `!`非空斷言運算符

const ele: HTMLElement = document.getElementById('#app')!

ele.addEventListener('click', (e: Event) => {
  //...
})

?? 空值合并運算符

|| 或 運算符是左側為 falsy 時,返回右側值。

null || 3 // 3
undefined || 3 // 3
false || 3 // 3
'' || 3 // 3
;-0 || 3 // 3
0 || 3 // 3
0n || 3 // 3
NaN || 3 // 3

?? 運算符是只有左側的值為 null 或 undefined 時,才返回右側的操作數(shù),否則返回左側的值。

null ?? 3 // 3
undefined ?? 3 // 3
false ?? 3 // false

注意:不能與 &&|| 操作符共用

空值合并運算符 ?? 不能直接與 &&|| 操作符組合使用。這種情況下會拋出 SyntaxError。

// '||' and '??' operations cannot be mixed without parentheses.(5076)
null || undefined ?? 'foo' // raises a SyntaxError

// '&&' and '??' operations cannot be mixed without parentheses.(5076)
true && undefined ?? 'foo' // raises a SyntaxError

但當使用括號來顯式表明優(yōu)先級時是可行的,比如:

;(null || undefined) ?? 'foo' // 'foo'

內置工具類型

聯(lián)合類型

Exclude<T, U>

Exclude<T, U> 提取存在于 T,但不存在于 U 的類型組成的聯(lián)合類型,通俗的說:從聯(lián)合類型中去除指定的類型。

type Keys = Exclude<'key1' | 'key2', 'key2'>
// type Keys = 'key1'

實現(xiàn):

type Exclude<T, U> = T extends U ? never : T

never 表示一個不存在的類型,而且與其他類型聯(lián)合后是沒有 never 的

Extract<T, U>

Extract<T, U> 提取聯(lián)合類型 T 和聯(lián)合類型 U 的所有交集。通俗地說:從聯(lián)合類型中提取指定的類型。

type Keys = Extract<'key1' | 'key2', 'key1'>
// type Keys = 'key1'

實現(xiàn):

type Extract<T, U> = T extends U ? T : never

NonNullable

NonNullable 的作用是從聯(lián)合類型中去除 null 或者 undefined 的類型。

type T = NonNullable<string | number | undefined | null> // => string | number

實現(xiàn):

type NonNullable<T> = Exclude<T, null | undefined>
// 或者
type NonNullable<T> = T extends null | undefined ? never : T

Record

Record<K, T> 用來構造一個類型,屬性為聯(lián)合類型中的每個子類型,屬性類型為 T。

type Eg = Record<'a' | 'b', string>
/**
  type Eg = {
    a: string
    b: string
  }
/*

實現(xiàn):

type Record<K extends keyof any, T> = {
  [P in K]: T
}

注意:keyof any 得到的是 string | number | symbol,原因在于 key 的類型只能為 string | number | symbol

操作接口類型

interface Person {
  name: string
  age?: number
  weight?: number
}

Partial<T>

將類型 T 所有屬性設置成可選的

type PartialPerson = Partial<Person>
// 相當于
interface PartialPerson {
  name?: string
  age?: number
  weight?: number
}

實現(xiàn):

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

將指定的 key 設置成可選的

type PratialOptionalPerson = PratialOptional<Person, 'name'>
//相當于
interface PratialOptionalPerson {
  name?: string
}

實現(xiàn):

type PratialOptional<T, K extends keyof T> = {
  [P in K]?: T[K]
}

Required

將給定類型的所有屬性變?yōu)楸剡x項

interface Person {
  name: string
  age?: number
  weight?: number
}

type RequiredPerson = Required<Person>
// 相當于
interface RequiredPerson {
  name: string
  age: number
  weight: number
}

Readonly

將類型 T 的所有屬性設置成只讀的

type ReadonlyPerson = Readonly<Person>
// 相當于
interface ReadonlyPerson {
  readonly name: string
  readonly age?: number
  readonly weight?: number
}

實現(xiàn):

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

將指定的 key 設置成只讀的:

type ReadonlyOptional<T, K extends keyof T> = {
  readonly [P in K]: T[P]
}

Pick

選取一組屬性,組成新的類型

type NewPerson = Pick<Person, 'name' | 'age'>
// 相當于
interface NewPerson {
  name: string
  age?: number
}

實現(xiàn):

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

Omit<T, K>

與 Pick 相反,Omit 是去除指定的屬性之后返回的新類型

type NewPerson = Omit<Person, 'weight'>
// 相當于
interface NewPerson {
  name: string
  age?: number
}

實現(xiàn):

  • 方式 1
type Omit = Pick<T, Exclude<keyof T, K>>
  • 方式 2
type Omit<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}

以上操作接口的工具類型都使用了映射類型。通過映射類型,可以對原類型的屬性進行重新映射組成新的的類型。

同態(tài)和非同態(tài)
  • Partial、Readonly 和 Pick 都屬于同態(tài)的,也就是其實現(xiàn)需要輸入類型 T 來拷貝屬性,因此屬性修飾符(readonly、?)都會被拷貝
type AP = Pick<{ readonly a?: string; b: number }, 'a'>

/**
  type AP = {
    readonly a?: string | undefined
  }
 */

以上可以看出屬性修飾符 readonly 和 ? 都被拷貝了。

  • Record 是非同態(tài)的,不需要拷貝屬性,因此不會拷貝屬性修飾符

為什么 Pick 拷貝了屬性,而 Record 沒有拷貝?

因為 Pick 的實現(xiàn)中,P in K(本質是 P in keyof T),T 為輸入的類型,而 keyof T 則遍歷了輸入類型;而 Record 的實現(xiàn)中,并沒有遍歷所有輸入的類型,K 只是約束為 keyof any 的子類型即可。
Pick、Partial、readonly 這幾個類型工具,無一例外,都是使用到了 keyof T 來輔助拷貝傳入類型的屬性。

函數(shù)類型

Parameters

獲取函數(shù)的參數(shù)類型,將每個參數(shù)類型放在一個元組中

type T0 = Parameters<() => void> // []

type T1 = Parameters<(x: number, y?: string) => void> // [x: number, y?: string]

實現(xiàn):

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never

ReturnType

用來獲取函數(shù)的返回類型

type T0 = ReturnType<() => void> // => void
type T1 = ReturnType<() => string> // => string

實現(xiàn):

type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any

ThisParameterType

用來獲取函數(shù)的 this 參數(shù)類型

type T = ThisParameterType<(this: Number, x: number) => void> // Number

實現(xiàn):

type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any
  ? U
  : unknown

OmitThisParameter

用來去除函數(shù)類型中的 this 類型。如果傳入的函數(shù)類型沒有顯式聲明 this 類型,那么返回的仍是原來的函數(shù)類型

type T = OmitThisParameter<(this: Number, x: number) => string> // (x: number) => string

實現(xiàn):

type OmitThisParameter<T> = unknown extends ThisParameterType<T>
  ? T
  : T extends (...args: infer A) => infer R
  ? (...args: A) => R
  : T

字符串類型

Uppercase

字符串字面量轉換成大寫字母

type T = Uppercase<'Hello'> // => 'HELLO'

Lowercase

字符串字面量轉換成小寫字母

type T = Lowercase<'HElO'> // => 'hello'

Capitalize

字符串的第一個字母轉為大寫字母

type T = Capitalize<'hello'> // => 'Hello'

Uncapitalize

字符串第一個字母轉成小寫

type T = Uncapitalize<'Hello'> // => 'hello'
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容