简体   繁体   中英

Typescript: only allow one occurrence of a value in an array

Is there an easy way to only allow one occurrence of a value in an array?

Lets say I have defined types like this:

  array: {
    value: boolean;
    label: string;
  }[];

I want to ensure that in that array, only one value can be true. If one value is true, the other values must be false. So for example I want to allow this:

const arr : array = [
    { value: false, label: '1' },
    { value: true, label: '2' },
    { value: false, label: '3' },
  ];

But I want typescript to fail and say that only one value can be true if I have an array like this:

const arr2 : array = [
    { value: true, label: '1' },
    { value: true, label: '2' },
    { value: false, label: '3' },
  ];

Is this possible in typescript?

Edit: To be sure, I only want to ensure this by types, not other logic

It is possible, however not really useful in the real world to do this at the type level (at least in TypeScript). Your best bet is to just use the built in Set for simple values or a Set library which uses deep equality (such as immutable-js ), which will allow you iterate through in insertion order and enforces that all items are unique.


For the sake of giving an answer which works using the typechecker though:

It's possible to use typed conditionals to return the type as never , thus ensuring the program doesn't compile. However, you'd need to know the values at compile time for this to be any use.

We can use a workaround for the fact TS does not have an equality operator using the technique descibed here: https://stackoverflow.com/a/53808212/7042389 . We can then use that to build an array of unique types, where if one matches another its type is returned as never - this will force a compilation error as no value can be assigned to never.

type IfNotEq<A, B, T> =
    (<T>() => T extends A ? 1 : 2) extends
    (<T>() => T extends B ? 1 : 2) ? never : T;

type UniqueArray2<A, B> = IfNotEq<A, B, [A, B]>;
type UniqueArray3<A, B, C> = IfNotEq<A, B, IfNotEq<B, C, IfNotEq<A, C, [A, B, C]>>>;

const xs1: UniqueArray2<"a", "b"> = ["a", "b"]; // works
const xs2: UniqueArray2<"a", "a"> = ["a", "a"]; // string is not assignable to never

const ys1: UniqueArray3<"a", "b", "c"> = ["a", "b", "c"];
const ys2: UniqueArray3<"a", "a", "b"> = ["a", "b", "c"]; // string is not assignable to never

Playground Link

I believe the only way to do this is through writing an exhaustive list of each possible scenario. You mention in your list that is will never be more than 10 - if you can make that a strict rule, then you can do something like this:

 type Array1 = {
  value: boolean;
  label: string;
}[];

type ArrayItem<T extends boolean> = {
  value: T,
  label: string;
}

type Array2 = [
  ArrayItem<true>,
  ArrayItem<false>,
  ArrayItem<false>,
] | [
  ArrayItem<false>,
  ArrayItem<true>,
  ArrayItem<false>,
] | [
  ArrayItem<false>,
  ArrayItem<false>,
  ArrayItem<true>,
];

const x: Array1 = [
  { value: true, label: '' },
  { value: true, label: '' },
  { value: true, label: '' }
]

const y: Array2 = [
  { value: false, label: '' },
  { value: false, label: '' },
  { value: true, label: '' }
];

// only z has a type error
const z: Array2 = [
  { value: true, label: '' },
  { value: false, label: '' },
  { value: true, label: '' }
]

Playground Link

Is there an easy way to only allow one occurrence of a value in an array?

It depends on your requirements.

This is the easiest/most readable way that I can think of, but it will allow only the first element of the array to be true, and all the others have to be false.

type normalObj = {
  value: false,
  label: string
}

type exceptionObj = {
  value: true,
  label: string
}

type normalObjWithOneException = [exceptionObj, ...normalObj[]];

const arr : normalObjWithOneException = [
    { value: true, label: '1' },
    { value: false, label: '2' },
    { value: false, label: '3' },
    { value: false, label: '4' },
    { value: false, label: '5' },
  ];

Playground Link

This can be a limitation if your wish is to provide an order-agnostic type. But if we start with inverting tuples order, then the answer is no , an "easy way" of doing it in TypeScript doesn't exist as of today (in JS we have plenty of options: sets, errors, proxies...).

If you care about an order-agnostic type, it is a topic that has been already discussed a lot (about inverting tuples):

  1. how to write an Invert type in typescript to invert the order of tuples
  2. Unordered tuple type
  3. Typescript: Unsorted tuple

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM