I'm trying to build a generic function that ensures specific properties of a class are set at initialization time. (Normally I'd just do this by making params required, but sometimes when you're working with dynamic data, that's not always possible!)
export const requireParams = (cls: AnyClass, requiredParams: string[]) => {
if (!requiredParams) {
return;
}
const missingParams = requiredParams.filter(
(param) => cls?.[param] === undefined || cls?.[param] === null || cls?.[param] === NaN
);
if (missingParams.length) {
throw new Error(`Missing required params: ${missingParams.join(", ")}`);
}
};
I'd then use it like:
class Foo {
constructor(public readonly food: string) {
requireParams(this, ['food']);
}
}
AnyClass
here is theoretical — and while I could make requireParams
generic and always pass in the type of cls
by <T>(cls: AnyClass<T>)
, this is kinda pedantic and in my brain, you should be able to infer T from the type of cls
?
I tried some wacko things like:
type AnyClass<T> = T extends new (...args: any[]) => infer R ? R : never;
(but always returns T as never
)
And I tried a bunch of other crazy ideas to try and infer T from the instance, like:
type GenericOf<T> = T extends new (...args: any) => InstanceType<infer X> ? X : T;
type AnyClass<T = any> = GenericOf<T extends new (...args: any) => infer X ? X : never>;
But it still comes back to the default value for T being any
, and thus assumes T is unknown
.
(This is, ultimately, because I don't know what I'm doing).
It also seems like I am grossly overthinking this - perhaps I need to type requireParams<T>
and give T a default value (and enforce that it is a class?)
Is this possible? If so, how? Or am I falling victim to this problem?
CodePudding user response:
Simplyfing down the problem to just use T
directly seems to (almost) solve your problem.
export const requireParams = <T,>(cls: T, requiredParams: (keyof T)[]) => {
if (!requiredParams) {
return;
}
const missingParams = requiredParams.filter(
(param) => cls?.[param] === undefined || cls?.[param] === null || cls?.[param] === NaN
);
if (missingParams.length) {
throw new Error(`Missing required params: ${missingParams.join(", ")}`);
}
};
But you also want to constrain T
be a class instance. AFAIK, there is nothing in the type system differentiating a class instance from any other object. I just tried your comment, but it seemingly does not to work for me :/
export type AnyClass = new <T extends { constructor: Function }>(...args: (keyof T)[]) => T;
const requireParams = <T extends InstanceType<AnyClass>>(
instance: T,
requiredParams?: (keyof T)[]
) => {
// ...
};
class Foo {
food: string;
constructor(food: string) {
this.food = food;
requireParams(this, ["food", "bar"]);
// ~~~~~
}
}
requireParams({}, ["food", "bar"]);
// ~~~~~~ ~~~~~
// This should complain on passing an empty {}, but doesn't.
// At least its params are caught as invalid though.
interface TastySnack {
snacks: string;
}
requireParams({ snacks: "crisps" } as TastySnack, ["snacks", "bar"]);
// ~~~~~