I have an interface (hereafter called Foo
) that contains a boolean property (hereafter called booleanProp
). Given that, I want to wrap it in a mapped type to be able to restrict the type of variables to either Foo
objects with the property booleanProp
set to true
, or false
.
Here's the example:
interface Foo {
readonly booleanProp: boolean;
}
type BooleanPropIsTrue<T extends { readonly booleanProp: boolean }> = {
readonly [ P in keyof T]: T[P];
} & {
readonly booleanProp: true;
};
const falseFoo: Foo = {
booleanProp: false
};
const trueFoo: Foo = {
booleanProp: true
};
if (falseFoo.booleanProp === true) {
// ERROR: type 'boolean' is not assignable to type 'true'.
const foo: BooleanPropIsTrue<Foo> = falseFoo;
}
if (trueFoo.booleanProp === true) {
// ERROR: type 'boolean' is not assignable to type 'true'.
const foo: BooleanPropIsTrue<Foo> = trueFoo;
}
if (trueFoo.booleanProp === true) {
// Works
const foo: BooleanPropIsTrue<Foo> = {
...trueFoo,
booleanProp: true
};
}
I would expected all 3 if clauses to work. Any suggestion?
(note: I'm aware of alternative options to avoid using mapped-types, no need to point that out)
The feature that this type of behavior of narrowing types based on checks is related to is discriminated unions. In the absence of a union Typescript will not keep track of such checks (since there is no potential type narrowing as far as the compiler is concerned). The simple work around would be to transform Foo
to be a discriminated union:
type Foo = {
booleanProp: true;
} | {
booleanProp: false;
}
type BooleanPropIsTrue<T extends { readonly booleanProp: boolean }> = {
readonly [ P in keyof T]: T[P];
} & {
readonly booleanProp: true;
};
// We introduce some randomness, if we assign { booleanProp: false } we can't even do the check as the compiler will know booleanProp is always false
const falseFoo: Foo = Math.random()> 0.5 ? {
booleanProp: false
} : {
booleanProp: true
};
const trueFoo: Foo = {
booleanProp: true
};
if (falseFoo.booleanProp === true) {
//Works
const foo: BooleanPropIsTrue<Foo> = falseFoo;
}
if (trueFoo.booleanProp === true) {
//Works
const foo: BooleanPropIsTrue<Foo> = trueFoo;
}
if (trueFoo.booleanProp === true) {
// Works
const foo: BooleanPropIsTrue<Foo> = {
...trueFoo,
booleanProp: true
};
}