Consider this class definition:
class MyClass<T extends Record<string, unknown> = any> {
constructor(
public params: {
methodOne: (data: Record<string, unknown>) => T;
methodTwo: (data: T) => void;
}
) {
// ...
}
}
// OK
const myClassInstance = new MyClass({
methodOne: (data) => {
return {
name: "foo",
lastName: "bar",
};
},
methodTwo: (data) => {
console.log(data.lastName); //ok
console.log(data.name); //ok
},
})
So what I'm trying to do is infer the data
parameter in the methodTwo
(which comes from the methodOne
and this works correctly in this kind of setup, when arguments are created inline
when instantiating the class instance.
Now I wanna do the same thing but instead of declaring the methodTwo
directly in the params argument to the MyClass
instance I want to have it somewhere else in the code, and this is where I get the dreaded (7022) error
// NOT OK
//'myClassInstanceTwo' implicitly has type 'any' because it does not have a type annotation
//and is referenced directly or indirectly in its own initializer.(7022)
const myClassInstanceTwo = new MyClass({
methodOne: (data) => {
return {
name: "foo",
lastName: "bar",
};
},
methodTwo: methodTwoExternal,
});
type MethodOneReturn<T extends MyClass> = ReturnType<T["params"]["methodOne"]>;
// 'data' is referenced directly or indirectly in its own type annotation.(2502)
function methodTwoExternal(data: MethodOneReturn<typeof myClassInstanceTwo>) {
console.log(data.lastName); //ok
console.log(data.name); //ok
}
Is there a way to make this work?
I'm open to changing the MyClass
constructor signature, maybe even introduce a factory
function for the class instantiation.
All I can think of here (assuming this pattern is something we need to support) would be to make some sort of builder that lets you set methodOne
first, then get an intermediate thing that lets you extract the type T
from it, and then lets you set methodTwo
and finally gives you a MyClass<T>
instance. Something like:
class MyClass<T extends Record<string, unknown> = any> {
static builder = {
m1<T extends Record<string, unknown>>(
methodOne: (data: Record<string, unknown>) => T) {
return {
__type: null! as T,
m2(methodTwo: (data: T) => void) {
return new MyClass({ methodOne, methodTwo })
}
}
}
}
constructor(public params: {
methodOne: (data: Record<string, unknown>) => T
methodTwo: (data: T) => void
}) { }
}
So MyClass.builder.m1(method1).m2(method2)
is the same as new MyClass(method1, method2)
, but now you can pause at MyClass.builder.m1(method1)
and inspect it for the type you care about. To make that easy I added a dummy/phantom __type
property, so that the type is like typeof xxx.__type
instead of something awful like Parameters<Parameters<typeof xxx.m2>[0]>[0]
.
Let's see if it works:
const myClassInstanceTwoBuilder =
MyClass.builder.m1((data) => {
return {
name: 'foo',
lastName: 'bar'
}
});
function methodTwoExternal(data: typeof myClassInstanceTwoBuilder.__type) {
console.log(data.lastName) //ok
console.log(data.name) //ok
}
const myClassInstanceTwo =
myClassInstanceTwoBuilder.m2(methodTwoExternal);
Looks good.