Constructor Destructuring with exposed parameters

后端 未结 1 1037
耶瑟儿~
耶瑟儿~ 2020-12-20 10:32

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 {
  c         


        
相关标签:
1条回答
  • 2020-12-20 10:52

    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

    0 讨论(0)
提交回复
热议问题