Currently, I am navigating a sophisticated custom typeguard library developed for a project I'm involved in. I am facing challenges in grasping the concept of function signatures used in typeguards.
The library includes a generic Is
function that has this structure:
type Is<A> = (a: unknown) => a is A
This setup allows me to create composable typeguards like:
const isString: Is<string> = (u: unknown): u is string => typeof u === 'string'
const isNumber: Is<number> = (u: unknown): u is number => typeof u === 'number'
There are also similar implementations for records, structs, arrays, and more. For instance, the array typeguard looks like this:
const isArray = <A>(isa: Is<A>) => (u: unknown): u is A[] => Array.isArray(u) && u.every(isa)
Then there's one specifically for objects:
export const isStruct = <O extends { [key: string]: unknown }>(isas: { [K in keyof O]: Is<O[K]> }): Is<O> => (
o
): o is O => {
if (o === null || typeof o !== 'object') return false
const a = o as any
for (const k of Object.getOwnPropertyNames(isas)) {
if (!isas[k](a[k])){
return false
}
}
return true
}
An example usage would be:
const isFoo: Is<{foo: string}> = isStruct({foo: isString})
We have a simplistic overloaded isIntersection function currently in place:
export function isIntersection<A, B>(isA: Is<A>, isB: Is<B>): (u: unknown) => u is A & B
export function isIntersection<A, B, C>(isA: Is<A>, isB: Is<B>, isC: Is<C>): (u: unknown) => u is A & B & C
export function isIntersection<A, B, C>(isA: Is<A>, isB: Is<B>, isC?: Is<C>) {
return (u: unknown): u is A & B & C => isA(u) && isB(u) && (!isC || isC(u))
}
The issue arises when adding additional typeguards beyond three since nesting isIntersection functions becomes necessary.
Building on insights from @jcalz, particularly his responses in the following link: Typescript recurrent type intersection, I came up with the following Intersection type:
type Intersection<A extends readonly any[]> =
A[number] extends infer U ?
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ?
I : never : never;
Based on this type, here is a potential implementation for the guard:
export function isIntersection<T extends any[]>(...args: { [I in keyof T]: Is<T[I]> }): Is<Intersection<T>>
{
return (u: unknown): u is Intersection<T[number]> => args.every((isX) => isX(u))
}
Although this solution works, the mystery lies in how the Intersection
type correctly infers the specific type.
I extend my appreciation to @jcalz for providing valuable answers and encouraging clarity in understanding these concepts.