2
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 ?

8
  • What is Coordinate? Please provide a minimal reproducible example. Commented May 9, 2022 at 20:06
  • it's just a class with x and y properties Commented May 9, 2022 at 20:07
  • 2
    Well, you can't restrict the input to be particular values. You can restrict to be particular types, and you can make the type of Coordinate generic so the numeric literal type of x and y are known at compile time. That gives you this behavior, where move(new Coordinate(1, 0)) will be accepted but move(new Coordinate(2, 3)) will not. Does that meet your needs? If so I could write up an answer; if not, please edit the question to demonstrate failed use cases. Commented May 9, 2022 at 20:11
  • Yes that almost meets my needs, I would have liked to not use generic type, but without generic new Coordinate(4, 10) would be accepted. Commented May 9, 2022 at 20:23
  • It's either that or you refactor to something that can be enumerated, like a union of string literals. Observe. Which solution do you prefer here? Commented May 9, 2022 at 20:27

2 Answers 2

4

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

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

3 Comments

I prefer the second approach, I think we can just write a simple enum Direction {Up, Right, Down, Left}, I don't really see the benefit of using a string enum
The benefit of using a string enum is that it doesn't have the downside of numeric enums; namely that all number values are assignable to them, see this q/a. That means it would accept move(12345) which you presumably don't want. String enums don't let you do that; move("abcde") will fail. Enums are essentially a legacy feature and lots of weird things happen there.
actually move("R") will fails whereas move(Direction.Right) works. Weird behavior !
0

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

2 Comments

I didn't want to update the Coordinate class. This class is used in other parts of the project so I can't make the constructor private
@OlivierBoissé Fair enough, suspected that this could be the case. The enum approach suggested by jcalz seems to be the best solution then.

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.