Skip to content

Instantly share code, notes, and snippets.

@bolencki13
Created October 2, 2023 18:58
Show Gist options
  • Save bolencki13/adcd51d78e0c363d18ebf11e4d309586 to your computer and use it in GitHub Desktop.
Save bolencki13/adcd51d78e0c363d18ebf11e4d309586 to your computer and use it in GitHub Desktop.
A match/when function set that deep partial matches values
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