I try to make some geometry in TypeScript. I'v got a lot of classes for elements (Point, PointIntersectionLL, Line, Circle...) and one class Figure.
I would like to do this code with autocompletion in VSCode :
const figure = new Figure()
const A = figure.create('Point', { x: 1, y: 2, color: 'blue' }
const B = figure.create('Point', { x: 1, y: 2, color: 'blue' }
figure.create('Line', { point1: A, point2: B })
So in Figure.ts I wrote this way with overloading :
const classes = {
Point,
Middle,
PointIntersectionLL,
Segment,
Circle,
CircleCenterDynamicRadius,
CircleCenterPoint,
Line,
Ray,
TextByPoint,
TextByPosition
}
create (type: 'Point', option: { x: number, y: number, shape?: 'x' | 'o' | '', size?: number }): Point
create (type: 'Line', option: { point1: Point, point2: Point, id?: string, color?: string, thickness?: number, isDashed?: boolean, hasToBeSaved?: boolean }): Line
create (type: 'Point' | 'Line', options: optionsPoint & optionsLine): Element2D {
return new classes[type](this, options)
}
Here, there is 2 overloads for Point and Line but I want to do it for over 30 elements (Ray, Polygon, Circle, Arc, particular points...). It's too long. How would you declare types in create
?
CodePudding user response:
Given a simpler setup like this:
class A { constructor(args: { a: number }) {} }
class B { constructor(args: { b: string }) {} }
const classes = { A, B }
You can create a function like this:
function create<T extends keyof typeof classes>(
typeStr: T,
...options: ConstructorParameters<typeof classes[T]>
): InstanceType<typeof classes[T]> {
return new classes[typeStr](...options)
}
This is a generic function which accepts a string that identifies the class to construct and saves that class construct as type T
.
The second argument is a spread of the ConstructorArguments
of that constructor. This arguments are spread back into the call to the constructor.
Lastly, the function is typed to return an InstanceType
of that constructor.
Which does what you want:
create('A', { a: 123 }) // fine
create('A', { b: 'a string' }) // type error
create('B', { a: 123 }) // type error
create('B', { b: 'a string' }) // fine
However, this line still has a type error:
return new classes[typeStr](options)
/*
Type 'A | B' is not assignable to type 'InstanceType<{ A: typeof A; B: typeof B; }[T]>'.
Type 'A' is not assignable to type 'InstanceType<{ A: typeof A; B: typeof B; }[T]>'.(2322)
Argument of type '[args: { a: number; }] | [args: { b: string; }]' is not assignable to parameter of type '{ a: number; } & { b: string; }'.
Type '[args: { a: number; }]' is not assignable to type '{ a: number; } & { b: string; }'.
Property 'a' is missing in type '[args: { a: number; }]' but required in type '{ a: number; }'.(2345)
*/
This is asking the typescript compiler to do a lot here, and it can't quite keep track of how all these types mesh together.
But, the inputs and output do ensure this is operation is type safe, so I would be inclined to just ignore the error.
function create<T extends keyof typeof classes>(
typeStr: T,
...options: ConstructorParameters<typeof classes[T]>
): InstanceType<typeof classes[T]> {
// @ts-expect-error This is _really_ hard to type properly.
return new classes[typeStr](...options)
}
I'm not sure how to make typescript happy with that, but for the arguments and return value of that function it would work exactly as you wish. So for one line functions like this with strong input/output types, I think a little type ignore is excusable.
Perhaps someone smarter than me improve on this answer.