I can't figure out how to type this in Typescript.
I have a Javascript class that I'm porting, simplified here:
class A {
constructor ({
a = 'hi',
b = 5,
...rest
} = {}) {
this.a = a
this.b = b
this.extra = rest
}
}
The constructor takes an options object with some defaults and gathers the rest into an object. That rest params object should be flat, no nested objects or arrays and needs to be JSON serializable, and is completely optional. But if it is present I would like to provide the caller an opportunity to have it be well-typed. So I defined a couple of types:
type KnownStuff = {
a: string
b: number
}
type FlatJSONCompatibleObject = {
[k: string]: string | number | boolean | null
} | {}
In addition, I want to be able to specify some additional parameters for the rest:
type Test = {
c: boolean
d: 'a' | 'b'
}
So I tried the following:
class A <T extends FlatJSONCompatibleObject = {}> {
a: string
b: number
extra: T
constructor ({
a = 'hi',
b = 5,
...rest
}: Partial<KnownStuff> & T = {}) {
this.a = a
this.b = b
this.extra = rest // fails
}
}
Which AFAICT fails because the generic isn't constrained. So then next:
class Foo <T extends Omit<FlatJSONCompatibleObject, keyof KnownStuff>> {
a: string
b: number
extra: T
constructor ({
a = 'hi',
b = 5,
...rest
}: Partial<KnownStuff> & T = {}) {
this.a = a
this.b = b
this.extra = rest
}
}
but that fails to compile, I can't get the generic constraint correct.
Closest I've been able to come is this:
class Bar<T extends Omit<FlatJSONCompatibleObject, keyof KnownStuff>> {
a: string
b: number
extra: Omit<FlatJSONCompatibleObject, keyof KnownStuff>
constructor ({
a = 'hi',
b = 5,
}: Partial<KnownStuff> = {}, rest: Partial<T> = {}) {
this.a = a
this.b = b
this.extra = rest
}
}
Which compiles but changes the signature which I'm really trying to avoid since this is for work and already in widespread use by multiple other teams. It's also wrong:
const c = new Bar<Test>()
That should fail as the properties on the parameter are not optional, but I had to use Partial<T>
to be able to assign the default empty object. I suspect I'm barking up the completely wrong tree here, which is why I'm asking this question.
So the constraints of the problem:
- Class takes an options object with some known properties.
- Those known properties should all have defaults,
new Whatever()
should work. - Class can also take some additional properties in the object.
- Extra stuff if present must be JSON compatible and flat (no nested objects or arrays).
- User of the class should be able to e.g. pass a type parameter so that the extra stuff has a well-defined and compiler-enforced type, i.e.
new Whatever<SomeTypeWithRequiredProperties>()
should not work. - I really don't want to change (from a Javascript POV) the signature of the constructor since this is already in widespread use.
How do I type this?
and a (not correct but hopefully gives the gist) test harness of sorts:
// class test harness
function test<T extends FlatJSONCompatibleObject = {}>(C: {new <U extends FlatJSONCompatibleObject = {}>(opts?: Partial<KnownStuff> & U): LP<T>}) {
// should work
const t1 = new C();
t1.extras // should be '{}'
const t2 = new C<{c: boolean}>({c: true});
t2.extras.c // should be true
// should be compile error
const t3 = new C<{c: boolean}>();
const t4 = new C<{c: boolean}>({d: 4});
}