Search code examples
typescriptmapped-types

Force a boolean property to be true or false using mapped types in Typescript


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)


Solution

  • 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
        };
    }