Created
October 2, 2023 18:58
-
-
Save bolencki13/adcd51d78e0c363d18ebf11e4d309586 to your computer and use it in GitHub Desktop.
A match/when function set that deep partial matches values
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type DeepPartial<T> = Partial<{ | |
[key in keyof T]: DeepPartial<T[key]> | ((obj: T[key]) => boolean) | |
}> | ((obj: T) => boolean); | |
type MatchHandlerFunc<T> = (obj: T) => void; | |
interface IMatch<T> { | |
when (matchObj: DeepPartial<T>, cb?: MatchHandlerFunc<T>): IMatch<T>; | |
default (cb: MatchHandlerFunc<T>): IMatch<T>; | |
evaluate (): void; | |
} | |
function match<T>( | |
obj: T | |
) { | |
/** | |
* Deep partial match with function resolution for boolean value | |
*/ | |
function isDeepMatch<T>(mainObj: T, matchObj: DeepPartial<T> | undefined) { | |
// Resolver function; should evaluate | |
if (typeof matchObj === 'function') { | |
return matchObj(mainObj); | |
} | |
// Non object type matching; includes null | |
if (typeof matchObj !== 'object' || matchObj === null) { | |
return mainObj === matchObj; | |
} | |
// Array matching | |
if (Array.isArray(matchObj) && Array.isArray(mainObj)) { | |
for (let index = 0; index < matchObj.length; index++) { | |
const matchSubObj = matchObj[index]; | |
const subObj = mainObj[index]; | |
if (matchSubObj === undefined) { | |
continue; | |
} | |
const matchResult = isDeepMatch(subObj, matchSubObj); | |
if (!matchResult) { | |
return false; | |
} | |
} | |
return true; | |
} | |
// Object key/value matching | |
for (const key in matchObj) { | |
const matchSubObj = matchObj[key]; | |
const subObj = mainObj[key]; | |
const matchResult = isDeepMatch(subObj, matchSubObj) | |
if (!matchResult) { | |
return false; | |
} | |
} | |
// Defaults to true | |
return true; | |
} | |
let defaultHandler: MatchHandlerFunc<T> | undefined; | |
const whenCases: Map<DeepPartial<T>, MatchHandlerFunc<T> | undefined> = new Map(); | |
const self: IMatch<T> = { | |
when: function (matchObj, cb) { | |
whenCases.set(matchObj, cb); | |
return this; | |
}, | |
default: function (cb) { | |
defaultHandler = cb; | |
return this | |
}, | |
evaluate: function () { | |
let hasMatch = false; | |
for (const [matchObj, cb] of whenCases.entries()) { | |
if (hasMatch) { | |
// We should find the next cb; we don't care about matching any further | |
if (cb) { | |
cb(obj); | |
return; | |
} | |
continue; | |
} | |
if (!isDeepMatch(obj, matchObj)) { | |
continue; | |
} | |
if (cb) { | |
cb(obj); | |
return; | |
} | |
hasMatch = true; | |
} | |
defaultHandler?.(obj); | |
} | |
}; | |
return self; | |
} | |
/** | |
* Examples | |
*/ | |
match(1) | |
.when(1, () => { | |
console.log('truthy') | |
}) | |
.default('no match') | |
.evaluate() | |
match([1,2,3]) | |
.when([], () => { | |
console.log('truthy') | |
}) | |
.default('no match') | |
.evaluate() | |
match([1,2,3]) | |
.when([]) // fall through case | |
.when([1], () => { | |
console.log('truthy') | |
}) | |
.default('no match') | |
.evaluate() | |
type UserFromDb = { | |
id: number, | |
name: string; | |
age: number; | |
} | null | |
match<UserFromDb>(null) | |
.when(null, () => console.log('No user found')) | |
.when({}, () => { | |
console.log('user found') | |
}) | |
.evaluate() | |
match<UserFromDb>({id: 5}) | |
.when(null, () => console.log('No user found')) | |
.when({ id: 5}, () => { | |
console.log('Specific found') | |
}) | |
.evaluate() | |
match<UserFromDb>({id: 5}) | |
.when(null, () => console.log('No user found')) | |
.when(obj => obj.age >= 18, () => { | |
console.log('An adult') | |
}) | |
.when(obj => obj.age < 18, () => { | |
console.log('A minor') | |
}) | |
.evaluate() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment