1

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:

  • array of objects
  • holds at least two or more objects
  • only one object has to have property active = true (all other objects may have active = false)
  • the object with property active = true can be placed at any index in the array (from index 0 to index array.length - 1 )

Thank you!!

5
  • That seems hard. Why not {selected: number, options: ListElement[]} Where selected is the index of the active element? Commented May 18, 2021 at 21:19
  • That would be a perfect solution for the custom select tag example. But that is just an example to try to keep it simple. It's not about building a custom "select-like" element, it's about creating a type of array of objects that requires one of the objects with property set to a value. I plan to use it to enforce at least an object to have a property set to an enum from a filtered list of Enums (not boolean and not an active property). Sorry if the example might be misleading. Commented May 18, 2021 at 21:48
  • 1
    That's impossible in TypeScript. Consider some data structure that does not impose requirements onto a type of array element based on properties of other array elements, or implement some runtime checks. Commented May 25, 2021 at 14:35
  • 1
    It's Typescript, it only "cares" about types, not the data in those types. Eventually when compiled, all that Typescript type-logic is gone. You have to write your own logic around it, it's your business logic. Typescript can not do that for you. Commented May 26, 2021 at 7:57
  • In my example, I am creating that Type. The only requirement that I am not respecting is that the object with property true can be placed at any index. How can you say that it's impossible while I am clearly showing an example of how to do it? You can say that one requirement is impossible, but not the Type itself. Commented May 26, 2021 at 9:27

1 Answer 1

1
+50

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?

Sign up to request clarification or add additional context in comments.

1 Comment

I agree with everything, except the last part (problem part). We could just recreate the array hence override the old array with a new (valid) one. That would be probably not the best in terms of performance but still possible and valid.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.