1

Let's say I have this code.

interface StateType<ExtendedBooleanType extends boolean, ExtendedLogType extends "is true" | "is false" | "both"> {
    bool: ExtendedBooleanType;
    log: ExtendedLogType,
}

type State =
    | StateType<true, "is true">
    | StateType<false, "is false">


const state: State = {
    bool: true,
    log: "is true"
}; // This runs and is expected, great!

const state: State = {
    bool: false,
    log: "is true"
}; // This doesn't run, and is expected, great!

But when I add this code, this doesn't work.

type StateList =
    | StateType<true, "is true">[]
    | StateType<false, "is false">[]
    | StateType<true & false, "both">[]

const state_list: StateList = [
    { bool: true, log: "both" },
    { bool: false, log: "both" },
]; // This doesn't work, and I don't know why :T

Ideally the StateList would recognize that the list has both true and false, and hence the log should be "both", but I can't get it to work.

I want to get the compiler to realize 3 cases, when the list has only true's, when the list has only false's and when the list has both, and make that a type similar to StateList.

Is there a way to make this work?

1 Answers1

2

true & false - gives you never. Type can't be true and at the same time false. Imagine number which can be 0 and at the same time 1. It is impossible.

StateList type should not be a union type. It should be conditional type. You should treat it as a function which checks whether list contains both true and false or not.

Furthermore, StateType type allows you to create invalid state StateType<true, "is false"> which is not cool.

If I were you, I would create a BooleanMap:

type BooleanMap = {
    true: true,
    false: false
}

Now, we can iterate through each key of the map and create appropriate state.

type Values<T> = T[keyof T]

type BooleanState<
    T extends Record<string, boolean>
    > = Values<{
        [Prop in keyof T]: {
            bool: T[Prop],
            log: `is ${Prop & string}`
        }
    }>

// type State = {
//     bool: true;
//     log: "is true";
// } | {
//     bool: false;
//     log: "is false";
// }
type State = BooleanState<BooleanMap>

Result is the same as yours. Ok, I know this is a bit verbose solution. Just wanted to show you this technique because some times it is very useful to make illegal state unrepresentable. I have used this pattern in several answers. See this

This solution is less verbose and uses power of distributive-conditional-types:

type MakeState<Bool extends boolean> =
    Bool extends true
    ? { bool: true, log: 'is true' }
    : { bool: false, log: 'is false' }

type State = MakeState<boolean>

If you want to express false & true === 'both', you should not do it as a part of union. It should be done in a validation function/type.

Using StateType<true & false, "both">[] as a part of our list state is a bit tricky, because this type should represent result of our validate function. We should obtain it after some calculations. This type can't be self represented in TypeScript without using conditional types.

In order to validate state_list we need extra function. Let's write a function and try to infer literal type of argument:

const validation = <
    Elem extends State,
    Tuple extends Elem[]
>(tuple: [...Tuple]) => tuple

validation([
    { bool: true, log: "is true" },
    { bool: true, log: "is true" },
])

If you hover your mouse on validation, you will see that provided argument is infered. If you are interested in inference you can check my article. If you wonder what this syntax [...Tuple] means - it is variadic tuples.

Ok, lets proceed. Now, when we have each element in the tuple infered, we can write Validate type. If you are interested in type validation of function arguments in typescript, you can check my articles: this and this.

type TupleMap<T extends State[],Result extends any[]=[]>=
    T extends [] 
    ? Result
    : T extends [infer Head,...infer Tail]
    ? Head extends State
    ? Tail extends State[]
    ? TupleMap<Tail,[...Result,{bool:Head['bool'],log:'both'}]>
    : never
    :never
    :never

type Validation<Tuple extends any[]> =
    boolean extends Tuple[number]['bool']
    ? TupleMap<Tuple>
    : Tuple

Validation expects tuple of State. boolean extends Tuple[number]['bool'] - means that if property bool of each element in the tuple is a union of true | false we need to remove log property and add another one with "both" value. In order to do it, we need to recursively iterate through each element in the tuple and override log property. Here, here and here you will find more explanation about tuple iteration.

Let's use Validation inside the function:

type MakeState<Bool extends boolean> =
    Bool extends true
    ? { bool: true, log: 'is true' }
    : { bool: false, log: 'is false' }

type State = MakeState<boolean>

type EdgeCaseState = { bool: boolean, log: 'both' }


type TupleMap<T extends State[],Result extends any[]=[]>=
    T extends [] 
    ? Result
    : T extends [infer Head,...infer Tail]
    ? Head extends State
    ? Tail extends State[]
    ? TupleMap<Tail,[...Result,{bool:Head['bool'],log:'both'}]>
    : never
    :never
    :never

type Validation<Tuple extends any[]> =
    boolean extends Tuple[number]['bool']
    ? TupleMap<Tuple>
    : Tuple

const validation = <
    Elem extends State,
    Tuple extends Elem[]
>(tuple: Validation<[...Tuple]>) => tuple

validation([
    { bool: true, log: "is true" },
    { bool: true, log: "is true" },
]) // no errors

validation([
    { bool: false, log: "is false" }, // expected error
    { bool: true, log: "is true" }, // expected error
])

validation([
    { bool: false, log: "both" }, // ok
    { bool: true, log: "both" }, // ok
]) 

Playground

TupleMap is a little verbose utility. It is possible to refactor it and use less verbose version:


type TupleMap<T extends State[]> = {
    [Prop in keyof T]: T[Prop] extends State ? { bool: T[Prop]['bool'], log: 'both' } : never
}

I know what you are going to ask:

Q: Is it possible to get rid of extra function ?

A: No. Please see this article. We need extra function in order to do validation.

Q: Is extra function affect code performance?

A: Please see this answer.