Home > other >  Type Definition : How to type an array to don't have tow specific value at the same time
Type Definition : How to type an array to don't have tow specific value at the same time

Time:02-13

I want to create a type that can only have those specific values and nothing else :

[0,1],[1,1],[1,0],[1,-1],[0,-1],[-1,-1],[-1,0],[-1,1]

So to achieve that, I have made this type definition :

type Steps = -1 | 0 | 1;
type Directions = [width:Steps, height:Steps];

But with this definition, I can do that :

const invalidDirection : Directions = [0,0];

And I don't know how to restrict that, especially for [0,0].

CodePudding user response:

There is no single, specific tuple type that corresponds to your intended Directions. The single specific tuple type [width: Steps, height: Steps] will, by necessity, allow the first element (labeled width) to be chosen independently of the second element (labeled height). And therefore nothing stops someone from choosing 0 for both elements.


If you want the two elements to depend on each other, you should write Directions as a union of acceptable types. You could do this manually, either as

type Directions = [width: Steps, height: Exclude<Steps, 0>] | 
  [width: Exclude<Steps, 0>, height: Steps];

or as

type Directions = [width: 0, height: Exclude<Steps, 0>] | 
  [width: Exclude<Steps, 0>, height: 0] |
  [width: Exclude<Steps, 0>, height: Exclude<Steps, 0>];

or as

type Directions = [width: 0, height: 1] | [width: 0, height: -1] | 
  [width: 1, height: 0] | [width: 1, height: 1] | 
  [width: 1, height: -1] | [width: -1, height: 0] | 
  [width: -1, height: 1] | [width: -1, height: -1];

(Note that while all three of those correspond to the same set of static values, they are not considered mutually equivalent by the compiler and may exhibit different behavior in different contexts; see Specializing Union Literal type property for some discussion)

Any of those result in the desired behavior:

let dir: Directions;
dir = [0, -1]; // okay
dir = [1, -1]; // okay
dir = [1, 0]; // okay
dir = [1, 1]; // okay
dir = [0, 1]; // okay
dir = [-1, 1]; // okay
dir = [-1, 0]; // okay
dir = [-1, -1]; // okay
dir = [0, 0]; // error!

The first two approaches use the Exclude<T, U> utility type to filter types out of a union. Note that the first one is terse enough that you may decide to use it directly.


On the other hand, you might want to do this programmatically from the definition of Steps, instead of manually... especially if you care about scaling the unions up or changing their values. Here's one way to do it:

type Directions = Exclude<
    { [W in Steps]: {
        [H in Steps]: [width: W, height: H] }[Steps]
    }[Steps], [0, 0]>;

We are Exclude-ing the bad [0, 0] member from the expression {[W in Steps]....}, so let's examine that. Here I'm making a nested "distributive object type" (as defined in microsoft/TypeScript#47109), which is making a mapped type and then immediately indexing into it to get a union. Let's look at the inner one, {[H in Steps]: [width: W, height: H]}[Steps]:

The mapped type iterates H over each union member of Steps and makes a new three-keyed object type like {"-1": [width: W, height: -1], "0": [width: W, height: 0], "1": [width: W, height: 1]}. We then index into that object type with the index [-1 | 0 | 1] which produces the three-member union [width: W, height: -1] | [width: W, height: 0] | [width: W, height: 1]. You can hopefully see how the outer distributed object type plugs in -1, 0, and 1 for W and produces a nine-element union.

And recall that we Exclude the [width: 0, height: 0] element from that union to get the eight-element union you desired!

Playground link to code

  • Related