Reputation: 1679
I want to create a Typescript Type for an array of objects. In this array of objects, I require only one object to have a property set to true.
You can probably solve it just with this typescript example, but I will provide a long and detailed explanation.
Let's say (just an example!) I want to recreate a <select>
tag (or a dropdown list).
My custom select has two have at least two options and I always want to have only one object to be active. I have three models:
abstract class SimpleDropDownListElement {
constructor(public label: string, public value: any) {}
}
class DropdownListElement extends SimpleDropDownListElement {
constructor(public label: string, public value: any, public active?: false) {
super(label, value);
}
}
class DropdownActiveListElement extends SimpleDropDownListElement {
active = true;
constructor(public label: string, public value: any) {
super(label, value);
}
}
I want to have an array with at least one (or more) DropdownListElement
(s) and one (always one - e.g. never 0 or 2+) DropdownActiveListElement
. Any order (object with active set to true can be everywhere in the array).
So my idea was to create a type like this:
type DropDownOptionsArray = [DropdownActiveListElement, DropdownListElement,
...Array<DropdownListElement>];
And that works, however, I need to have the object with the active property set to true as the first element of my array.
So my idea was to reverse the array (not very smart), but I still get problems if the array holds more than 3 values.
type Reverse<Tuple> = Tuple extends [infer A, ...infer B]? [...Reverse<B>, A] : [];
const dropInvertedWithInvertedType: Reverse<DropDownOptionsArray> =
[new DropdownListElement('b', 'b'), new DropdownActiveListElement('a', 'a')];
const dropInvertedWithInvertedType1: Reverse<DropDownOptionsArray> =
[new DropdownListElement('b', 'b'), new DropdownActiveListElement('a', 'a'),
new DropdownListElement('b', 'b')]; // errors
Then I started to go crazy with rest elements (hoping for TS v4 to help me with some magic):
type DropDownOptionsArray = [...[DropdownActiveListElement],
...Array<DropdownListElement>, ...Array<DropdownListElement>];
// OK
const twoEntries: DropDownOptionsArray = [new DropdownActiveListElement('a', 'a'),
new DropdownListElement('b', 'b')];
const fourEntries: DropDownOptionsArray = [new DropdownActiveListElement('a', 'a'),
new DropdownListElement('b', 'b'), new DropdownListElement('b', 'b'),
new DropdownListElement('b', 'b')];
// should not error - but errors
const twoEntriesRandomPos: DropDownOptionsArray = [new DropdownListElement('b', 'b'),
new DropdownActiveListElement('a', 'a'), new DropdownListElement('b', 'b')];
const twoEntriesRandomPos: DropDownOptionsArray = [new DropdownListElement('b', 'b'),
new DropdownListElement('b', 'b'), new DropdownActiveListElement('a', 'a')];
// should error
const twoActiveEntries: DropDownOptionsArray = [new DropdownActiveListElement('a', 'a'),
new DropdownListElement('b', 'b'), new DropdownActiveListElement('a', 'a')];
const noActiveEntry : DropDownOptionsArray = [new DropdownListElement('b', 'b')]; // should have a different error
Writing overloads verbosely is not feasible, we could have an array of 20+ elements.
To summarize, I would need this Type:
active = true
(all other objects may have active = false
)active = true
can be placed at any index in the array (from index 0
to index array.length - 1
)Thank you!!
Upvotes: 1
Views: 4611
Reputation: 161
I believe this is impossible, and I have created some examples to validate the assertion.
By way of explanation, it is true that you can a define variable-length tuple type with multiple fixed types at the start, eg
type A = number;
type B = string;
type C = null;
type X = [B, ...Array<A>];
const x:X = ["", 1, 2, 3];
const x[0] = "";
A variable of type X must start with a string and be followed by 0 or more numbers, and this is correctly type checked when assigning the whole array. In addition, assigning to x[0] will be type checked as a string.
You can also define one or more fixed types at the end, eg
type X = [B, ...Array<A>, C];
const x:X = ["", 1, 2, 3, null];
x[0] = "";
x[4] = 4;
This time, type-checking is still performed when assigned to the array as a whole. But, it cannot type-check an assignment to the last element. The best it can do is to type check that elements after the first are A|C. This is because typescript can only do compile-time checks, not runtime checks.
Finally, anything in the type definition between explicit types at the start and end simply get converted into a union of all the types, eg
type X = [B, B, ...Array<A>, B, ...Array<A>, C, C];
is treated the same as
type X = [B, B, ...Array<A|B>, C, C];
which can be seen by hovering over X in an editor.
So you cannot define an array with only 1 value of a specific type, and if you could it couldn't be policed at compile-time anyway.
And surely there would be a problem if it could be achieved. When updating the active element would you not temporarily have an array of 0 or 2 active elements, ie invalid, because the updates cannot be performed atomically?
Upvotes: 1