网站搜索

typescript - 限制函数参数的可能取值


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

是否可以强制将 direction 参数设置为 Up、Right、Down、Left 之一?

答案#1

为了将 TypeScript 函数的输入限制为几种可能的值之一,你需要能够在类型级别识别这些值。这类类型需要是“单元”或“字面量”类型,只允许特定的值,然后你可以将你的限制表示为这些字面量类型的联合。

在 TypeScript 中,原始类型有字面量类型。例如,字符串字面量类型"hello"就是字符串字面量"hello"的类型,其他字符串都不能赋值给它。TypeScript 中的其他此类单位类型包括数值字面量类型如123、布尔字面量类型如true,以及特殊类型null和undefined。还有unique symbol。同时也有字面量数值和字符串枚举。

不幸的是,TypeScript 没有“字面量”对象类型的概念。因此,你不能真正定义一个 Up 类型,它是特定值 const Up=new Cooordinate(0, -1) 的类型。无论你做什么,你都可以创建另一个不同的对象 AlsoUp(即在运行时 AlsoUp !== Up),并且 TypeScript 会在任何需要 Up 的地方接受它。


一种可能的方法是将 Coordinate 的 x 和 y 属性的数字字面量类型做成泛型,如下所示:

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

然后当你构建你的方向时,你可以在类型层次上区分它们:

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>

而你的 Direction 类型是这些的联合:

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

这将防止你使用不是你想要的 x 和 y 坐标调用 move():

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

它将允许你使用你批准的值之一调用 move():

move(Right); // okay

但是,出于必要,它也将允许你使用与批准的方向值具有相同 x 和 y 坐标的不同对象来调用 move():

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

这可能不是一个大问题;毕竟,虽然新的对象在引用身份上不是严格相等的,但在属性方面它是“等价”的。


如果你真的只想允许 move() 被调用时使用四个特定值之一,那么你不能使用对象类型。 作为替代,你可以使用字面量类型(比如,例如,一个字符串枚举)作为键,这些键指向你批准的对象:

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

现在绝对不可能使用不同的坐标调用 move(),因为它只接受 Direction 字符串枚举的值。

代码的 Playground 链接

答案#2

你可以做以下操作,不过这确实有一些限制:

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
  • 你不能直接将 direction 与某个常量进行相等比较,因为这将比较的是实例,而不是值。
  • 你现在有一个带泛型的类型可以携带。

操场

这些限制也可以这样减轻:

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)

操场