Home > Net >  Typescript: tuple with no duplicates
Typescript: tuple with no duplicates

Time:11-22

Problem definition

Assume we have a React component C that accepts properties Props. Props have a field named edges. Edges are defined as a tuple of length 1-4 composed of string literals top, bottom, left, right.

Task: restrict the edges param to a tuple with no duplicates.

E.g.:

This should compile fine:

<C edges={['top', 'bottom']} />

while this should fail:

<C  edges={['top', 'top']} />

What I have so far


// Our domain types
type Top = 'top';
type Bottom = 'bottom';
type Left = 'left';
type Right = 'right';
type Edge = Top | Bottom | Left | Right;

// A helper types that determines if a certain tuple contains duplicate values
type HasDuplicate<TUPLE> = TUPLE extends [infer FIRST, infer SECOND]
    ? FIRST extends SECOND
        ? SECOND extends FIRST
            ? true
            : false
        : false
    : TUPLE extends [first: infer FIRST, ...rest: infer REST]
    ? Contains<FIRST, REST> extends true
        ? true
        : HasDuplicate<REST>
    : never;

// Just some helper type for convenience
type Contains<X, TUPLE> = TUPLE extends [infer A]
    ? X extends A
        ? A extends X
            ? true
            : false
        : false
    : TUPLE extends [a: infer A, ...rest: infer REST]
    ? X extends A
        ? A extends X
            ? true
            : Contains<X, REST>
        : Contains<X, REST>
    : never;

With the above I can already get this:

type DoesNotHaveDuplicates = HasDuplicate<[1, 2, 3]>; // === false
type DoesHaveDuplicates = HasDuplicate<[1, 0, 2, 1]>; // === true

Where I am stuck

Let's say we have a component C:


// For simple testing purposes, case of a 3-value tuple
type MockType<ARG> = ARG extends [infer T1, infer T2, infer T3]
    ? HasDuplicate<[T1, T2, T3]> extends true
        ? never
        : [T1, T2, T3]
    : never;

interface Props<T> {
    edges: MockType<T>;
}

function C<T extends Edge[]>(props: Props<T>) {
    return null;
}

The above works but only like this:

// this compiles:
<C<[Top, Left, Right]> edges={['top', 'left', 'right']} />

// this does not (as expected):
<C<[Top, Left, Left]> edges={['top', 'left', 'left']} />

What I cannot figure out is how to get rid of the generics in component instantiation and make typescript deduce the types at compile time based on the value provided to the edges property.

CodePudding user response:

I don't really see the point of MockType. So let's get rid of it.

Instead, use a conditional to check if HasDuplicate<T> is false. If it is, we can set the type of edges to be [...T].

interface Props<T extends Edge[]> {
    edges: HasDuplicate<T> extends false ? [...T] : never;
}

The variadic tuple syntax is important here as it hints to the compiler that we want to infer T as a tuple. Otherwise T will be inferred to be an array with a union of Edge as its type.

// these compile
const a = (
  <>
    <C<[Top, Left, Right]> edges={['top', 'left', 'right']} />
    <C edges={['top', 'left', 'right']} />
  </>
)

// these do not compile
const b = (
  <>
    <C<[Top, Left, Left]> edges={['top', 'left', 'left']} />
    <C edges={['top', 'left', 'left']} />
  </>
)

One a site note: We can also simplify your HasDuplicate type a bit.

type HasDuplicate<TUPLE extends any[]> = 
  TUPLE extends [infer L, ...infer R]
    ? L extends R[number]
      ? true
      : HasDuplicate<R>
    : false

Playground

CodePudding user response:

Alternatiwe ways, without explicit generic and less utility types.

If you have predefined allowed strings for edges, you can consider this approach:

import React from "react"

type Top = 'top';
type Bottom = 'bottom';
type Left = 'left';
type Right = 'right';
type Edge = Top | Bottom | Left | Right;

// https://github.com/microsoft/TypeScript/issues/13298#issuecomment-692864087
type TupleUnion<U extends string, R extends any[] = []> = {
    [S in U]: Exclude<U, S> extends never ? [...R, S?] : TupleUnion<Exclude<U, S>, [...R, S?]>;
}[U];


interface Props {
    edges: TupleUnion<Edge>
}

function C(props: Props) {
    return null;
}

(
    <>
    // this compiles:
        <C edges={['top', 'left', 'right']} />

    // this does not (as expected):
        <C edges={['top', 'left', 'left']} />

    // this compiles:
        <C edges={['top', 'left', 'right']} />

    // this does not
        <C edges={['top', 'left', 'left']} />
    </>
)

Playground

If you want to allow any string, you can use this approach:

import React from "react"

type WithDuplicates<Keys extends string[], Acc extends string[] = []> =
  Keys extends []
  ? Acc
  : Keys extends [infer First extends string, ...infer Tail extends string[]]
  ? First extends Acc[number]
  ? WithDuplicates<Tail, [...Acc, never]>
  : WithDuplicates<Tail, [...Acc, First]>
  : Keys

interface Props<T extends string[]> {
  edges: T
}

function C<E extends string, Edges extends E[]>(props: Props<WithDuplicates<[...Edges]>>) {
  return null;
}

(
  <>
    // this compiles:
    <C edges={['top', 'left', 'right']} />

    // this does not (as expected):
    <C edges={['top', 'left', 'left']} />

    // this compiles:
    <C edges={['top', 'left', 'right']} />

    // this does not
    <C edges={['top', 'left', 'left']} />
  </>
)

playground

  • Related