Home > Net >  Restrict possible values for function parameter
Restrict possible values for function parameter

Time:05-10

class Coordinate {
  constructor(readonly x: number, readonly y: number) {}
}

const Up = new Coordinate(0, -1);
const Right = new Coordinate(1, 0);
const Down = new Coordinate(0, 1);
const Left = new Coordinate(-1, 0);

// I would like direction to be Up or Right or Down or Left
function move(direction: Coordinate) {
    ...
}

Is it possible to force the direction parameter to be one of Up, Right, Down, Left ?

CodePudding user response:

In order to restrict a TypeScript function input to one of several possible values, you would need to be able to identity such values at a the type level. Such types would need to be "unit" or "literal" types which only admit specific values, and then you could represent your restriction as the union of such literal types.

In TypeScript there are literal types for primitive. For example, the string literal type "hello" is the type of the string literal "hello", and no other string is assignable to it. Other such unit types in TypeScript are numeric literal types like 123, boolean literal types like true, and the special types null and undefined. There's also unique symbol. And there are literal numeric and string enums also.

Unfortunately, TypeScript doesn't have the concept of "literal" object types. So you can't really define an Up type which is the type of the specific value const Up = new Cooordinate(0, -1). No matter what you do, you'll be able to come up a distinct other object AlsoUp (i.e, AlsoUp !== Up at runtime) which is accepted by TypeScript wherever Up is.


One possible approach is instead to make Coordinate generic in the numeric literal types of the x and y properties, like this:

export class Coordinate<X extends number, Y extends number> {
  constructor(readonly x: X, readonly y: Y) { }
}

Then when you construct your directions, you can tell the difference between them at the type level:

export const Up = new Coordinate(0, -1); // Coordinate<0, -1>
export const Right = new Coordinate(1, 0); // Coordinate<1, 0>
export const Down = new Coordinate(0, 1); // Coordinate<0, 1>
export const Left = new Coordinate(-1, 0); // Coordinate<-1, 0>

And your Direction type is the union of these:

export type Direction = typeof Up | typeof Right | typeof Down | typeof Left
// type Generic.Direction = Coordinate<0, -1> | Coordinate<1, 0> | 
// Coordinate<0, 1> | Coordinate<-1, 0>

function move(direction: Direction) { }

This will prevent you from calling move() with coordinates whose x and y are not the ones you want:

move(new Coordinate(4, 10)); // error

And it will allow you to call move() with one of your approved values:

move(Right); // okay

But, by necessity, it will also allow you to call move() with a different object with the same x and y coordinates as the approved direction values:

move(new Coordinate(1, 0)); // also okay

This might not be a big problem; after all, while the new object isn't strictly equal in terms of reference identity, it is "equivalent" in terms of its properties.


If you really want to only allow move() to be called with one of four specific values, then you can't use object types. As an alternative, you could use literal types (like, for example, a string enum) as keys which point to your approved objects:

enum Direction {
  Up = "U",
  Right = "R",
  Down = "D",
  Left = "L"
}

export const directionMap = {
  [Direction.Up]: new Coordinate(0, -1),
  [Direction.Right]: new Coordinate(1, 0),
  [Direction.Down]: new Coordinate(0, 1),
  [Direction.Left]: new Coordinate(-1, 0)
} as const


function move(dirName: Direction) {
  const direction = directionMap[dirName];
}

move(Direction.Right); // okay

Now it's definitely impossible to call move() with a different coordinate, because it only accepts the values of the Direction string enum.

Playground link to code

CodePudding user response:

You could do the following, which does have some limitations though:

class Coordinate<X extends number, Y extends number> {
  constructor(
    readonly x: X,
    readonly y: Y
  ) {}
}

const Up = new Coordinate(0, -1);
const Right = new Coordinate(1, 0);
const Down = new Coordinate(0, 1);
const Left = new Coordinate(-1, 0);

type Direction =
  typeof Up |
  typeof Right |
  typeof Down |
  typeof Left;

function move(direction: Direction) {
  // ...
}

move(Up);
move(new Coordinate(1, 0));
move(new Coordinate(1, 1)); // Error
  • You cannot do direct equality comparison between direction and one of the constants, because it would compare instances, not the values.
  • You now have a type with generics to haul around.

Playground

These limitations could also be mitigated like this:

class Coordinate {
  readonly #key = {}
  private constructor(
    readonly x: number,
    readonly y: number
  ) {}

  static Up = new Coordinate(0, -1);
  static Right = new Coordinate(1, 0);
  static Down = new Coordinate(0, 1);
  static Left = new Coordinate(-1, 0);
}

const { Up, Right, Down, Left } = Coordinate;

function move(direction: Coordinate) {
  // ...
}

move(Up);
move({ x: 1, y: 0 }); // Error (Property '#key' is missing...)
move(new Coordinate(1, 0)); // Error (Constructor of class 'Coordinate' is private)

Playground

  • Related