I am currently wading through a complex custom typeguard library written for a project I'm working on, and I'm having problems understanding the way that function signatures work for typeguards.
There is a generic Is
function that takes the following form:
type Is<A> = (a: unknown) => a is A
This allows me to write composable typeguards of the form
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 ones for records, structs, arrays and so on. For example, the array one is
const isArray = <A>(isa: Is<A>) => (u: unknown): u is A[] => Array.isArray(u) && u.every(isa)
And the one used for objects is
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
}
For example:
const isFoo: Is<{foo: string}> = isStruct({foo: isString})
We currently have a very basic overloaded isIntersection function:
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))
}
Naturally, the problem is that if you want to have a fourth or fifth typeguard added to this, you need to nest isIntersection typeguards, which isn't great.
Based on some great answers by @jcalz, particularly Typescript recurrent type intersection, I have the following 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;
And I think I can write the actual guard as something like this:
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))
}
This works, but I don't know how the Intersection
type is able to correctly infer the type.
I'm deeply grateful to @jcalz for answers and for pushing me to be clearer.