在專案開發中,我們時常會遇到判斷某個變數是否為一個有效值,或者根據變數的型別,根據不同的型別進行不同的操作的情況。
比如最常見的,判斷一個變數是否為 Truthy
值(什麼是 Truthy 值):
if (value !== null && value !== undefined) {
// 搞事情
}
咋看之下,也就短短兩個語句,但這個事情需要進行 10 次、100 次的時候,或許你會開始想到封裝:
function isDefined(value) {
return value !== undefined && value !== null
}
既然有了這個想法,何不一干到底,我們就直接來封裝一個自己的 is
方法庫。
下列的方法,將使用標準的TypeScript
編寫,你將會看到:泛型、型別謂詞is
。
一些常規的 is
方法
通過型別謂詞 is
可以在 TypeScript
收窄型別,幫助更好的型別推斷,這裡不展開。
判斷 Truthy
和 Falsy
:
// 可以思考一下 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
}
判斷其他基本型別(除了 null
和 undefined
):
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
}
一些碎碎念
最近回想了這幾年的工作,發現自己封裝過各種各樣的工具函式,但很多都是零零散散地遍佈在專案中。
也是出於整理和複習的目的,想著分享一下自己寫過的一些東西,於是便嘗試寫了這篇文章,希望能幫助到一些人。