【封裝小技巧】is 系列方法的封裝

未覺雨聲發表於2022-05-07

在專案開發中,我們時常會遇到判斷某個變數是否為一個有效值,或者根據變數的型別,根據不同的型別進行不同的操作的情況。

比如最常見的,判斷一個變數是否為 Truthy 值(什麼是 Truthy 值):

if (value !== null && value !== undefined) {
  // 搞事情
}

咋看之下,也就短短兩個語句,但這個事情需要進行 10 次、100 次的時候,或許你會開始想到封裝:

function isDefined(value) {
  return value !== undefined && value !== null
}

既然有了這個想法,何不一干到底,我們就直接來封裝一個自己的 is 方法庫。

下列的方法,將使用標準的 TypeScript 編寫,你將會看到:泛型、型別謂詞 is

一些常規的 is 方法

通過型別謂詞 is 可以在 TypeScript 收窄型別,幫助更好的型別推斷,這裡不展開。

判斷 TruthyFalsy

// 可以思考一下 value !== undefined 和 typeof value !== 'undefined' 有什麼區別?
// null 呢?
function isDefined<T = unknown>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null
}

function isNull(value: unknown): value is null | undefined {
  return value === undefined || value === null
}

判斷其他基本型別(除了 nullundefined):

function isNumber(value: unknown): value is number {
  return typeof value === 'number'
}

// 提問:NaN 是不是一個基本型別呢?
function isNaN(value: unknown): value is number {
  return Number.isNaN(value)
}

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

function isBoolean(value: unknown): value is boolean {
  return typeof value === 'boolean'
}

// 嚴格判斷 true
function isTrue(value: unknown): value is true {
  return value === true
}

// 嚴格判斷 false
function isFalse(value: unknown): value is false {
  return value === false
}

// 別忘了 Symbol
function isSymbol(value: unknown): value is symbol {
  return typeof value === 'symbol'
}

// 還有一個基本型別,它是誰呢?

除開基本型別後,接下來就是一些常見物件型別的判斷的,在這個之前可以先思考一個問題:

typeof object === 'object' 能不能有效的判斷一個變數是否為物件呢?

從廣義上來講,只要這個成立,那該變數確實是一個物件,但這往往不是我們所需要和期望的,因為這樣並不能區分陣列 [] 和 物件 {} 的區別,包括一些其他物件如 Date

所以我們要藉助一個大家都知道的繞一點的方式來判斷:Object.prototype.toString,這裡我們就直接上了,不知道具體原理的請自行搜尋。

常見物件的判斷:

// 存一下,減少物件屬性的讀取
const toString = Object.prototype.toString

function is(value: unknown, type: string) {
  return toString.call(value) === `[object ${type}]`
}

// 這裡可以思考物件型別的收窄,用 Record<string, any> 是否合適?
function isObject<T extends Record<string, any> = Record<string, any>>(
  value: unknown
): value is T {
  return is(value, 'Object')
}

// 陣列可以使用原生的方法獲得更高的效率
function isArray(value: unknown): value is any[] {
  return Array.isArray(value)
}

// 插播一個 function
function isFunction(value: unknown): value is (...any: any[]) => any {
  return typeof value === 'function'
}

// 補充上面被遺忘的 BigInt 基本型別
function isBigInt(value: unknown): value is bigint {
  return typeof value === 'bigint'
}

// 這裡如果想要同時支援 PromiseLike 的型別收窄的話要怎麼寫呢?
function isPromise(value: unknown): value is Promise<any> {
  return (
    !!value &&
    typeof (value as any).then === 'function' &&
    typeof (value as any).catch === 'function'
  )
}

function isSet(value: unknown): value is Set<any> {
  return is(value, 'Set')
}

function isMap(value: unknown): value is Map<any, any> {
  return is(value, 'Map')
}

function isDate(value: unknown): value is Date {
  return is(value, 'Date')
}

function isRegExp(value: unknown): value is RegExp {
  return is(value, 'RegExp')
}

注意到這裡單獨封裝了一個 is 方法,這個方法是可以進行任意的擴充的,比如想判斷一些自定義類的時候,可以基於該 is 再封裝(上面的方法都是這個原則):

function isMyClass(value: unknown): value is MyClass {
  return is(value, 'MyClass')
}

一些不太常規的 is 方法

除了一些型別的判斷,我們時常會有出現像是判斷該變數是否為一個 Empty 值:

什麼是 Empty 指的,常規一點來講就是包括:空陣列、空字串、空 Map、空 Set、空物件 {}
function isEmpty(value: unknown) {
  if (Array.isArray(value) || typeof value === 'string') {
    return value.length === 0
  }

  if (value instanceof Map || value instanceof Set) {
    return value.size === 0
  }

  if (isObject(value)) {
    return Object.keys(value).length === 0
  }

  return false
}

還有一個比較常見的場景是,判斷某個變數是否是否個物件的鍵值,我們可以藉助 Object.prototype.hasOwnProperty 來判斷:

const hasOwnProperty = Object.prototype.hasOwnProperty

function has(value: Record<string, any>, key: string | symbol): key is keyof typeof value {
  return hasOwnProperty.call(value, key)
}

整合一下

好了,到此為止一個包含了基本型別和一些常見型別的 is 函式庫就大功告成了,最後附上一份整合後的完整程式碼,大家可以在這個基礎上做一些自己的擴充(應該沒人需要純 js 版本的吧):

const toString = Object.prototype.toString
const hasOwnProperty = Object.prototype.hasOwnProperty

export function is(value: unknown, type: string) {
  return toString.call(value) === `[object ${type}]`
}

export function has(value: Record<string, any>, key: string | symbol): key is keyof typeof value {
  return hasOwnProperty.call(value, key)
}

export function isDefined<T = unknown>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null
}

export function isNull(value: unknown): value is null | undefined {
  return value === undefined || value === null
}

export function isNumber(value: unknown): value is number {
  return typeof value === 'number'
}

export function isNaN(value: unknown): value is number {
  return Number.isNaN(value)
}

export function isString(value: unknown): value is string {
  return typeof value === 'string'
}

export function isBoolean(value: unknown): value is boolean {
  return typeof value === 'boolean'
}

export function isTrue(value: unknown): value is true {
  return value === true
}

export function isFalse(value: unknown): value is false {
  return value === false
}

export function isSymbol(value: unknown): value is symbol {
  return typeof value === 'symbol'
}

export function isBigInt(value: unknown): value is bigint {
  return typeof value === 'bigint'
}

export function isArray(value: unknown): value is any[] {
  return Array.isArray(value)
}

export function isObject<T extends Record<string, any> = Record<string, any>>(
  value: unknown
): value is T {
  return is(value, 'Object')
}

export function isPromise(value: unknown): value is Promise<any> {
  return (
    !!value &&
    typeof (value as any).then === 'function' &&
    typeof (value as any).catch === 'function'
  )
}

export function isFunction(value: unknown): value is (...any: any[]) => any {
  return typeof value === 'function'
}

export function isSet(value: unknown): value is Set<any> {
  return is(value, 'Set')
}

export function isMap(value: unknown): value is Map<any, any> {
  return is(value, 'Map')
}

export function isDate(value: unknown): value is Date {
  return is(value, 'Date')
}

export function isRegExp(value: unknown): value is RegExp {
  return is(value, 'RegExp')
}

export function isEmpty(value: unknown) {
  if (Array.isArray(value) || typeof value === 'string') {
    return value.length === 0
  }

  if (value instanceof Map || value instanceof Set) {
    return value.size === 0
  }

  if (isObject(value)) {
    return Object.keys(value).length === 0
  }

  return false
}

一些碎碎念

最近回想了這幾年的工作,發現自己封裝過各種各樣的工具函式,但很多都是零零散散地遍佈在專案中。

也是出於整理和複習的目的,想著分享一下自己寫過的一些東西,於是便嘗試寫了這篇文章,希望能幫助到一些人。

相關文章