I'm trying to combine a list of assertions in a type-safe way without having to call them all manually.
Example code:
type Base = { id: string, name?: string, age?: number };
type WithName = { name: string };
type WithAge = { age: number };
export type Asserter<T, U> = (x: T) => asserts x is T & U;
const assertName: Asserter<Base, WithName> = (x: Base): asserts x is Base & WithName => {
if (x.name === undefined) {
throw new Error("missing name");
}
};
const assertAge: Asserter<Base, WithAge> = (x: Base): asserts x is Base & WithAge => {
if (x.age === undefined) {
throw new Error("missing age");
}
};
type ExtractAssertions<T, U extends Asserter<T, any>[]> =
U extends [Asserter<T, infer V>, ...infer Rest]
? Rest extends Asserter<T, any>[]
? V & ExtractAssertions<T, Rest>
: V
: {};
function multiAssert<T, A extends Asserter<T, any>[]>(
item: T,
assertions: A
): asserts item is T & ExtractAssertions<T, A> {
assertions.forEach(assertion => assertion(item));
}
const data: Base = { id: "aas-aa", name: "frank", age: 30 };
multiAssert(data, [assertName, assertAge]);
console.log(data.name[0]); // This should compile correctly
console.log(data.age + 3); // This should also be possible
I'm doing something wrong in the ExtractAssertions
type, but I can't figure it out for the life of me.
Here's a link to a TS playground
I've tried a lot of variations on the recursive ExtractAssertions
type, but they all either ended up back at the Base
type, or led to never
.
Your ExtractAssertions
type is fine (although you can likely get it to work without being a recursive conditional type). The problem seems to be that when you call multiAssert()
, the type of assertions
and therefore A
is inferred too widely for your purposes and is just an unordered array type. But you want it to be a tuple type so it feeds into ExtractAssertions
as intended.
You can affect this inference by making A
a const
type parameter, which gives TypeScript a hint that you want narrower inference:
function multiAssert<T, const A extends Asserter<T, any>[]>(
item: T,
assertions: A
): asserts item is T & ExtractAssertions<T, A> {
assertions.forEach(assertion => assertion(item));
}
And now it works as intended:
const data: Base = { id: "aas-aa", name: "frank", age: 30 };
multiAssert(data, [assertName, assertAge]);
// ^? function multiAssert<Base, [Asserter<Base, WithName>, Asserter<Base, WithAge>]>(⋯)
data
//^? const data: Base & WithName & WithAge