简体   繁体   中英

TypeScript: pass the ReturnType of a nested function in an object that uses Interface

I want to create a structure where I have an Interface like this:

interface PageData {
    data: (this: Page) => object;
    methods: (this: Page) => { [methodName: string]: () => any };
    main: (this: Page) => void;
}

and a class that uses this data to create a new instance:

class Page {
    name: string;
    data: object;
    methods: object;
    constructor(name: string, data: PageData) {
        this.name = name;
        this.data = data.data.call(this);
        this.methods = data.methods.call(this);
        data.main.call(this);
    }
}

Now how would I pass the ReturnType of the data and methods functions to Page.data and Page.methods? I searched for ReturnType, Generics and so on and at this point im just confused. Maybe its not even possible? I want to have this Interface so I can use it in many files. My folder structure would be something like:

src/ src/main.ts src/Page.ts src/pages/demo.ts

In demo.ts i want to export an object with the interface PageData:

export default {
    data: function() {
        return {
            customName: this.name + "demo"
        }
    },
    methods: function() {
        return {
            doSomething: () => this.data.customName.repeat(2)
        }
    },
    main: function() {
        console.log(this.data.customName);
        console.log(this.methods.doSomething());
    }
} as PageData

In main.ts I would get the data with require. Like new Page("demo", require("src/pages/demo.ts")) .

I tried to find any example that resembles that what I am trying to do but I could not find anything. The only things I found was how to get the ReturnType of a function, but not a function that is in an object that uses an Interface.

With this complete example there are three errors:
Line 27, 31: Property 'customName' does not exist on type 'object'.
Line 32: Property 'doSomething' does not exist on type 'object'.

interface PageData {
        data: (this: Page) => object;
        methods: (this: Page) => { [methodName: string]: () => any };
        main: (this: Page) => void;
}

class Page {
        name: string;
        data: object;
        methods: object;
        constructor(name: string, data: PageData) {
                this.name = name;
                this.data = data.data.call(this);
                this.methods = data.methods.call(this);
                data.main.call(this);
        }
}

export default {
        data: function() {
                return {
                        customName: this.name + "demo"
                }
        },
        methods: function() {
                return {
                        doSomething: () => this.data.customName.repeat(2)
                }
        },
        main: function() {
                console.log(this.data.customName);
                console.log(this.methods.doSomething());
        }
} as PageData

My suggestion is to make your PageData and Page types generic in the types of a Page object's data and methods properties, like this:

interface PageData<D extends object, M extends Record<keyof M, () => any>> {
  data: (this: Page<D, M>) => D;
  methods: (this: Page<D, M>) => M;
  main: (this: Page<D, M>) => void;
}

class Page<D extends object, M extends Record<keyof M, () => any>> {
  name: string;
  data: D;
  methods: M;
  constructor(name: string, data: PageData<D, M>) {
    this.name = name;
    this.data = data.data.call(this);
    this.methods = data.methods.call(this);
    data.main.call(this);
  }
}

(Note: given that your example PageData is explicitly using arrow function properties instead of methods inside its methods property, I'm not going to bother with the ThisType<T> utility type , as they don't have their own this context to worry about.)


Then there's the issue with making a PageData object and resolving those errors.

I assume you don't want to hand-annotate the this parameter of all of its properties, and instead would like the compiler to infer the appropriate this type. Unfortunately, that probably won't be possible all at once with a single object (see relevant GitHub comment ); the compiler would need to both build and infer the type at the same time, which it's not great at. It's better at being able to do things sequentially. So, if, as in your example, the data property isn't really dependent on anything, the methods property is dependent on data , and the main property is dependent on both data and methods , then you could consider making a builder for PageData objects that creates them sequentially:

const pageDataBuilder = {
  data: <D extends object>(data: (this: Page<any, any>) => D) => ({
    methods: <M extends Record<keyof M, () => any>>(methods: (this: Page<D, any>) => M) => ({
      main: (main: (this: Page<D, M>) => void) => ({
        data, methods, main
      })
    })
  })
};

And here's how you could use it:

const data = pageDataBuilder.data(
  function () {
    return {
      customName: this.name + "demo"
    }
  }).methods(function () {
    return {
      doSomething: () => this.data.customName.repeat(2)
    }
  }).main(function () {
    console.log(this.data.customName);
    console.log(this.methods.doSomething());
  });

That compiles with no errors, and then this works:

const page = new Page("demo", data);
page.data.customName; // string

Looks good to me. Hope that helps; good luck!

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM