MaxiJonson
MaxiJonson

Reputation: 635

TypeScript - How do you infer a class generic type from a method's parameter?

I am trying to type a class generic from a method argument that will be called later. The generic type of the class would not be known until we called a method taking a generic argument. Then, for any other methods, the generic type would be passed on.

Honestly, for me, this seems so complex of functionality that I am not even sure TypeScript has a way of doing it...

Here's what I would like the behavior to look like:

class Class<T> {
    foo = (bar: T[]) => {
        /* ... */
        return this;
    };

    baz = (cb: (qux: T) => void) => {
        /* ... */
        return this;
    };
}

new Class() // Here, T should be unknown
    .foo([{ name: "John", id: "123" }]) // Here, it should be infered that T is { name: string, id: string }
    .baz((person) => null) // Here, person should have the type { name: string, id: string }

In this example, when we instantiate the Class, T should be unknown. Then, when we pass an array of objects to foo, the T should be inferred to be of that object type, since bar is typed T[]. Now that T has been infered, qux should be automatically typed when passing a function to baz. ( baz(cb: ({ name: string, id: string }) => void) )

Note that I do not want to have to pass a generic to the Class as I want it to be inferred later. In other words, I don't want to do

new Class<{ name: string, id: string }>()

Thanks for your answers!

Upvotes: 3

Views: 1178

Answers (1)

MaxiJonson
MaxiJonson

Reputation: 635

I guess writing the question acted as rubber duck debugging! About a few minutes after my post, I found this. However, it is not too pretty, so I am going to leave the thread open for a while in case a better idea comes up:

class Class<T> {
    foo = <U extends T>(bar: U[]) => {
        /* ... */
        return (this as unknown) as Class<U>; // not pretty
    };

    baz = <U extends T>(cb: (qux: U) => void) => {
        /* ... */
        return (this as unknown) as Class<U>; // not pretty
    };
}

new Class() // Here, T is unknown, as it should be
    .foo([{ name: "John", id: "123" }]) // Here, T becomes U ({name: string, id: string})
    .baz((person) => null); // Here, person is typed {name: string, id: string} Yay!

// This also works!
new Class() // Here, T is unknown, as it should be
    .baz((person: { name: string; id: string }) => null) // Here, T becomes U ({name: string, id: string})
    .foo([{ name: "John", id: "123" }]); // Here, bar is typed { name: string, id: string }[] Yay again!

By typing the methods with another generic type that extends the class' generic type, we can return this and "safely" type it as Class<U>, which now gives an actual type instead of unknown.

The problem here is that this only works because we are returning this while typing it with a new type Class<U>. Thus, passing an object to a method that does not return this would not modify it's generic type...

class Class<T> {
    public data: T;

    quux = <U extends T>(quuz: U) => {
        this.data = quuz;
        /* ... void */
    };
}

const c = new Class();
c.quux({ name: "John", id: "123" });
c.data // still unknown

Upvotes: 1

Related Questions