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.