如何让配置对象始终作为构造函数及静态方法的最后参数(TypeScript)
问题场景
我搭建了如下TypeScript代码结构,可通过Tile对象或静态方法传入坐标创建Grid实例:
type Point = { x: number, y: number } type TileConfig = { thickness: number; color: string; } type GridConfig = { origin: Point; gutter: number; } type TileConstructor<T extends Tile> = new (coord: Point, config?: TileConfig) => T class Tile { config: TileConfig constructor(coord: Point, {thickness = 1, color = 'BLUE'}: TileConfig = {thickness: 1, color: 'BLUE'}) { this.config = { thickness, color }; } } class TileWithTerrain extends Tile { terrain!: string } class Grid<T extends Tile>{ tiles = new Map<string, T>(); setTile(tile: T) { this.tiles.set(tile.toString(), tile); }; config: GridConfig static fromPoints<T extends Tile>( coords: Iterable<Point>, tileConstructor?: TileConstructor<T>, { origin = {x:0, y:0}, gutter = 0 }: GridConfig | Partial<GridConfig> | undefined = {origin: {x:0, y:0}, gutter: 0 } ) {} constructor(); constructor(tiles: Iterable<Tile>, config?: GridConfig); constructor(tiles?: Iterable<T>, { origin = {x:0, y:0}, gutter = 0 }: GridConfig = {origin: {x:0, y:0}, gutter: 0 }) { this.config = {origin, gutter} ; }; } const grid = Grid.fromPoints([{x:0, y:0}], { origin: { x: 0, y: 10 }}) // ERROR: 希望用户能直接用标准Tile和配置创建实例 const grid2 = Grid.fromPoints<TileWithTerrain>([{x:0, y:0}], TileWithTerrain, { origin: { x: 0, y: 10 }}) // 正常工作
目前调用Grid.fromPoints([{x:0, y:0}], { origin: { x: 0, y: 10 }})会触发类型错误,原因是可选参数tileConstructor与GridConfig类型冲突。我希望无需让用户传入undefined或Tile构造函数,就能让配置对象始终作为最后参数传递,寻求最优实现方案。
我不想让用户必须传入undefined或Tile才能使用坐标创建实例:
const grid = Grid.fromCoordinates([{ x: 0, y: 0 }], Tile, { offset: -1 })
解决方案
利用TypeScript的函数重载定义清晰的调用签名,让编译器自动区分参数场景,实现无需额外传参的友好调用方式:
class Grid<T extends Tile>{ tiles = new Map<string, T>(); setTile(tile: T) { this.tiles.set(tile.toString(), tile); }; config: GridConfig // 重载1:仅传坐标+配置(默认使用Tile构造函数) static fromPoints(coords: Iterable<Point>, config?: GridConfig | Partial<GridConfig>): Grid<Tile>; // 重载2:传坐标+自定义Tile构造函数+配置 static fromPoints<T extends Tile>(coords: Iterable<Point>, tileConstructor: TileConstructor<T>, config?: GridConfig | Partial<GridConfig>): Grid<T>; // 统一实现逻辑 static fromPoints<T extends Tile>( coords: Iterable<Point>, arg2?: TileConstructor<T> | GridConfig | Partial<GridConfig>, arg3?: GridConfig | Partial<GridConfig> ): Grid<T | Tile> { // 初始化默认值 let tileConstructor: TileConstructor<T | Tile> = Tile as TileConstructor<T | Tile>; let finalConfig: GridConfig = { origin: {x:0, y:0}, gutter: 0 }; // 区分参数场景 if (typeof arg2 === 'function') { tileConstructor = arg2; finalConfig = { ...finalConfig, ...arg3 }; } else if (arg2) { finalConfig = { ...finalConfig, ...arg2 }; } // 创建Grid并生成Tile const grid = new Grid<T | Tile>(undefined, finalConfig); for (const coord of coords) { grid.setTile(new tileConstructor(coord)); } return grid; } constructor(); constructor(tiles: Iterable<Tile>, config?: GridConfig); constructor(tiles?: Iterable<T>, { origin = {x:0, y:0}, gutter = 0 }: GridConfig = {origin: {x:0, y:0}, gutter: 0 }) { this.config = {origin, gutter} ; }; } // 两种调用方式均正常工作 const grid = Grid.fromPoints([{x:0, y:0}], { origin: { x: 0, y: 10 }}) // OK const grid2 = Grid.fromPoints<TileWithTerrain>([{x:0, y:0}], TileWithTerrain, { origin: { x: 0, y: 10 }}) // OK
方案说明
- 通过函数重载明确两种调用模式,消除参数歧义;
- 实现逻辑中通过判断参数类型自动分支处理,默认使用
Tile构造函数; - 配置对象采用合并方式,保留默认值的同时覆盖用户传入的配置项;
- 用户无需额外传入
undefined或Tile,即可直接用坐标+配置创建Grid实例。
内容的提问来源于stack exchange,提问作者ChrisOram
相关产品推荐
相关产品推荐

