Búsqueda de sitios web

typescript - Restringir valores posibles para el parámetro de la función


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) {
    ...
}

¿Es posible forzar el parámetro direction a ser uno de Up, Right, Down, Left?

Respuesta #1

Para restringir la entrada de una función de TypeScript a uno de varios valores posibles, necesitarías poder identificar tales valores a nivel de tipo. Tales tipos necesitarían ser tipos "unit" o "literal" que solo admiten valores específicos, y entonces podrías representar tu restricción como la unión de tales tipos literales.

En TypeScript hay tipos literales para los primitivos. Por ejemplo, el tipo literal de cadena "hello" es el tipo del literal de cadena "hello", y ninguna otra cadena se le puede asignar. Otros tipos unitarios similares en TypeScript son los tipos literales numéricos como 123, los tipos literales booleanos como true, y los tipos especiales null y undefined. También existe unique symbol. Y también hay enums literales numéricos y de cadena.

Desafortunadamente, TypeScript no tiene el concepto de tipos de objeto "literales". Por lo tanto, realmente no puedes definir un tipo Up que sea el tipo del valor específico const Up=new Cooordinate(0, -1). No importa lo que hagas, podrás crear otro objeto distinto AlsoUp (es decir, AlsoUp !== Up en tiempo de ejecución) que sea aceptado por TypeScript dondequiera que se espere un Up.


Un enfoque posible es, en su lugar, hacer que Coordinate sea genérico en los tipos de literales numéricos de las propiedades x y y, así:

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

Entonces, cuando construyas tus direcciones, puedes distinguir entre ellas a nivel de tipo:

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>

Y tu tipo Direction es la unión de estos:

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) { }

Esto impedirá que llames a move() con coordenadas cuyo x y y no sean los que deseas:

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

Y te permitirá llamar a move() con uno de tus valores aprobados:

move(Right); // okay

Pero, por necesidad, también te permitirá llamar a move() con un objeto diferente con las mismas coordenadas x y y que los valores de dirección aprobados:

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

Esto podría no ser un gran problema; después de todo, aunque el nuevo objeto no sea estrictamente igual en términos de identidad de referencia, es "equivalente" en términos de sus propiedades.


Si realmente quieres permitir que move() se llame solo con uno de cuatro valores específicos, entonces no puedes usar tipos de objetos. Como alternativa, podrías usar tipos literales (como, por ejemplo, un enum de cadenas) como claves que apunten a tus objetos aprobados:

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

Ahora es definitivamente imposible llamar a move() con una coordenada diferente, porque solo acepta los valores del enum de cadenas Direction.

Enlace de Playground al código

Respuesta #2

Podrías hacer lo siguiente, aunque tiene algunas limitaciones:

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
  • No se puede hacer una comparación de igualdad directa entre direction y una de las constantes, porque compararía instancias, no los valores.
  • Ahora tienes un tipo con genéricos para transportar.

Parque infantil

Estas limitaciones también podrían mitigarse así:

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)

Parque infantil