I am probably experimenting with the limits of TypeScript's system here but is it at all possible to properly define makeIsBox
to create isBoxA
by composing over isA
?
type A = 'A'
type B = 'B'
type Letter = A | B
const makeIsLetter = <L extends Letter>(checkLetter: L) => (letter: Letter): letter is L => letter === checkLetter
const isA = makeIsLetter('A')
const isB = makeIsLetter('B')
// Test
const takeA = (a: A) => void 0
const takeB = (b: B) => void 0
declare const a: Letter
declare const b: Letter
takeA(a) // should fail
if(isA(a)) {
takeA(a) // should not fail
} else {
takeA(a) // should fail
takeB(a) // should not fail>
}
// So far so good.
type BoxA = { type: A, value: string }
type BoxB = { type: B, status: number }
type Box = BoxA | BoxB
const makeIsBox = <
L extends Letter,
Fn extends (letter: Letter) => letter is L
>(
fn: Fn
) => <B extends Box>(box: Box) => fn(box.type)
const isBoxA = makeIsBox(isA)
declare const box: Box
if (isBoxA(box)) {
const value = box.value
}
TypeScript won't infer a user-defined type guard for you. You have to annotate it yourself, like this:
const makeIsBox = <L extends Letter>(fn: (letter: Letter) => letter is L) => (
box: Box
): box is Extract<Box, { type: L }> => fn(box.type);
This still only needs to be generic in L
. It takes a function of type (l: Letter)=>l is L
and returns a function of type (b: Box)=>b is Extract<Box, {type: L}>
. That's using the Extract
utility type to select just the member of the Box
union which is assignable to {type: L}
.
It should work the way you want:
const isBoxA = makeIsBox(isA);
declare const box: Box;
if (isBoxA(box)) {
const value = box.value;
} else {
const status = box.status;
}
Hope that helps; good luck!