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)
操场
