There's no simple way in TypeScript to programmatically copy properties from an object passed into a constructor to the object being constructed and have the compiler verify that it's safe. You can't do it with destructuring; that won't bring names into scope unless you mention them, and that even if you could do this you'd have to manually copy them into the constructed object anyway.
Similar in effect to destructuring is the function Object.assign()
, so you could hope to have the constructor be like constructor(x: X){Object.assign(this, x)}
... and this does work at runtime. But the compiler does not recognize that the properties have actually been set, and will tend to warn you:
class FailedPageConfig implements PageConfigArgs { // error!
// Class 'FailedPageConfig' incorrectly implements interface 'PageConfigArgs'.
// Property 'getList' is missing in type 'FailedPageConfig'
// but required in type 'PageConfigArgs'.
constructor(config: PageConfigArgs) {
Object.assign(this, config);
}
}
You can manually fix that by using a definite assignment assertion for all "missing" properties, but this is a declaration you wanted to avoid, right?
class OkayPageConfig implements PageConfigArgs {
getList!: PageConfigArgs["getList"]; // definite assignment
constructor(config: PageConfigArgs) {
Object.assign(this, config);
}
}
So, what else can we do?
One thing we can do is make a function that generates class constructors that use Object.assign()
, and use a type assertion to tell the compiler not to worry about the fact that it can't verify the safety:
function ShallowCopyConstructor<T>() {
return class {
constructor(x: T) {
Object.assign(this, x);
}
} as new (x: T) => T; // assertion here
}
And then you can use it like this:
export class PageConfigPossiblyUndefinedIsSliding extends ShallowCopyConstructor<
PageConfigArgs
>() {}
declare const pcfgX: PageConfigPossiblyUndefinedIsSliding;
pcfgX.getList; // pagingInfo: PagingInfo) => Observable<any>
pcfgX.isSliding; // boolean | undefined
You can see that PageConfigPossiblyUndefinedIsSliding
instances are known to have a getList
and an isSliding
property. Of course, isSliding
is of type boolean | undefined
, and you wanted a default false
value, so that it would never be undefined
, right? Here's how we'd do that:
export class PageConfig extends ShallowCopyConstructor<
Required<PageConfigArgs>
>() {
constructor(configArgs: PageConfigArgs) {
super(Object.assign({ isSliding: false }, configArgs));
}
}
declare const pcfg: PageConfig;
pcfg.getList; // pagingInfo: PagingInfo) => Observable<any>
pcfg.isSliding; // boolean
Here PageConfig
extends ShallowCopyConstructor<Required<PageConfigArgs>>()
, meaning that the superclass's constructor requires both getList
and isSliding
properties to be passed in (using the Required<T>
utility type).
And the constructor of PageConfig
only needs a PageConfigArgs
, and assembles a Required<PageConfigArgs>
from it using another Object.assign()
call.
So now we have a PageConfig
class whose constructor accepts PageConfigArgs
and which constructs a Required<PageConfigArgs>
.
Finally we get to your UsersPage
class. You can't do new PageConfig({this.getList})
. That's not valid syntax. Instead you can do this:
class UsersPage {
config = new PageConfig(this);
getList(pagingInfo: PagingInfo) {
return null!;
}
}
or this
class UsersPage {
config = new PageConfig({getList: this.getList});
getList(pagingInfo: PagingInfo) {
return null!;
}
}
or, if you don't want to type the word getList
twice, and don't want to copy every property from this
, then you can make a helper function called pick
which copies named properties out of an object:
function pick<T, K extends keyof T>(obj: T, ...keys: K[]) {
const ret = {} as Pick<T, K>;
for (const k of keys) {
ret[k] = obj[k];
}
return ret;
}
And then use this:
class UsersPage {
config = new PageConfig(pick(this, "getList"));
getList(pagingInfo: PagingInfo) {
return null!;
}
}
Okay there's a lot to unpack there. Hope it gives you some direction. Good luck!
Link to code