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
Coordinate? Please provide a minimal reproducible example.xandypropertiesCoordinategeneric so the numeric literal type ofxandyare known at compile time. That gives you this behavior, wheremove(new Coordinate(1, 0))will be accepted butmove(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.new Coordinate(4, 10)would be accepted.