Home > Net >  Infer any (generic) TypeScript class type from argument passed to a function
Infer any (generic) TypeScript class type from argument passed to a function

Time:10-30

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(", ")}`);
  }
};

Playground


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"]);
//                                                           ~~~~~

Playground

OP's Playground

  • Related