-1

I have a constructor which uses destrucuring to simplify what needs to be passed to create the object with the proper defaults.

export class PageConfig {
  constructor({
    isSliding = false
  }: {
    isSliding?: boolean;
    getList: (pagingInfo: PagingInfo) => Observable<any>;
  }) {}
}

I want to expose the properties passed into the constructor as public, preferable without redeclaring them. Additionally I would not like to have a middle man object. e.g say I had a class which instanciated my object like so:

class UsersPage {

    config = new pageConfig({ this.getList })    

    getList(pagingInfo: PagingInfo) {
      // do work...
    }
}

I would like the config to expose 2 thing:

config.getList()
config.isSliding

How can I efficiently do this?

EDIT I've tried to do this by creating an base class which both the consturtor arguments and the class inherits from, However, if I omit the properties in the destructuring I won't be able to reference them in the constructor.

e.g

export class PageConfigArgs {
    isSliding?: boolean;
    getList: (pagingInfo: PagingInfo) => Observable<any>;
}

export class PageConfig extends PageConfigArgs {
  constructor({
    isSliding = false
  }: PageConfigArgs ) {
    super();
    this.isSliding = isSliding;
    //this fails since its not declared in the destructuring
    //However I do not want to declare it since it is required
    this.getList = getList;  
  }
}
johnny 5
  • 19,893
  • 50
  • 121
  • 195
  • "I do not want to declare it since it is required"... but that's not how object destructuring works. – jcalz Aug 25 '19 at 01:08
  • Possible duplicate of [How do I destructure all properties into the current scope/closure in ES2015?](https://stackoverflow.com/questions/31907970/how-do-i-destructure-all-properties-into-the-current-scope-closure-in-es2015) – jcalz Aug 25 '19 at 01:08
  • 1
    And [again](https://stackoverflow.com/questions/57634041/typescript-destructuring-with-required-parameter?noredirect=1#comment101721550_57634041), `new PageConfig({this.getList})` is not valid syntax. You have to choose between `new PageConfig(this)` and `new PageConfig({getList: this.getList})`. – jcalz Aug 25 '19 at 01:28

1 Answers1

2

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for your answer this should work. The main reason I want to use destructuring is because the settings class actually has 10 params and I most of the parameters are optional, but sometimes you may need to to set the last optional parameter. I which means you would have to pass the other optional to change the last one. I also didn't want to have to configure the settings after instanciation, I wish I could just call the constructor with named parameters and avoid destructuring altogether – johnny 5 Aug 25 '19 at 03:21
  • so there in type script there is no way to have skipable optional parmeters mixed with required ones? – johnny 5 Aug 25 '19 at 14:04
  • 1
    I'm not sure what you're asking... the `PageConfig` implementation I provided allows you to have skippable optional parameters mixed with required ones, and shows to get the "default value for optional parameters" behavior (`Object.assign({ isSliding: false }, configArgs)`). The short answer is that `Object.assign()` is generally more suitable to what you're doing than destructuring is. – jcalz Aug 25 '19 at 14:15
  • thanks I think. I've been depiciting this as an X Y problem. I've created a new question which explains what I want [here](https://stackoverflow.com/q/57646880/1938988) – johnny 5 Aug 25 '19 at 14:26
  • It seems to me that you're asking the same question again, but in my above implementation the code does exactly what you're asking for. If you do `const u = new UsersPage(); console.log(u.config)` you will see that `u.config` contains both an `isSliding` value of `false` plus the `getList()` function from the `UsersPage` class (not that it will properly bound to the right `this` if you need that). The `getList` property is set. What, specifically, is the issue you have here? I'd love to see exactly where the code in this answer fails to meet your use case in this and your other questions. – jcalz Aug 25 '19 at 14:33
  • [Code relevant to other question](https://stackblitz.com/edit/typescript-8xnb8s?file=index.ts), feel free to play with it and see where it fails for you. – jcalz Aug 25 '19 at 14:54
  • 1
    I Kinda get it now, My original confusion was on the limitation that all of the properties you want to pass have to be inside the object you are picking from. from your example you used pick. e.g `new PageConfig(pick(this, "getList"));` I wanted pass isSliding true, seperately, but as a work around I can just set it on the class that was passing it in e.g `isSliding = true` and then `new PageConfig(pick(this, "getList, isSliding"));` – johnny 5 Aug 25 '19 at 15:38
  • This will work perfectly, I wish I could upvote this twice. – johnny 5 Aug 25 '19 at 15:39
  • You *could* just do `new PageConfig(this)`, and it would copy *every* (own enumerable) property... but I just can't tell if you want that behavior or not. – jcalz Aug 25 '19 at 15:46
  • 1
    *Or* you could do `new PageConfig(Object.assign(pick(this, "getList"), {isSliding: true}))` – jcalz Aug 25 '19 at 15:47