x instanceof class type passed through parameter

Viewed 570

I am trying to determine whether a variable is a child class of a parameter type class in a function. This is done for a system to ensure authentication with a role based method.

I have one script with a list of user types and a factory method to assign a class:

abstract class UserType {}

abstract class EmpRoleType extends UserType {}
abstract class ClientRoleType extends UserType {}

class EmpSupport extends EmpRoleType {}
class ClientSupport extends ClientRoleType {}

function getUserType(role: string): UserType | null {
  switch(role) {
    case 'EmpSupport': 
      return EmpSupport;
    case 'ClientSupport': 
      return ClientSupport;

    default: 
      return null;
  }
}

To find whether a user is authenticated with the correct role I have this function:

 // Pass in parent type of role to allow. Eg. EmpRoleType
function allowPermission(allowParentRole: UserType, userRole: UserType) {
  return userRole instanceof allowParentRole;
}

However this gives me an error: The right-hand side of an 'instanceof' expression must be of type 'any' or of a type assignable to the 'Function' interface type.

Even trying to instantiate a class from the getUserType fails:

const x = new getUserType('EmpSupport')();

Error: 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.

UPDATE:

Using captain-yossarian answer: How would I find map a string variable onto one of those classes assuming I didn't know the value of the string? Eg.

get UserType(): UserType | null {
    let storageUserType = localStorage.getItem(AuthService.authStorageItems.userType);
    if (!storageUserType) {
      return null;
    }

    try {
      return ClassMap[storageUserType]; // or
      return GetUserType(storageUserType);
    } catch (error) {
      return null;
    }
  }
2 Answers

First of all we need to know that typeof UserType and UserType are two different types.

First type typeof UserType is a type of class constructor. Second type UserType is a type of UserType class instance.

In this case, I think it is better to use HashMap data structure instead of switch. Consider this:

abstract class UserType { }

abstract class EmpRoleType extends UserType { }
abstract class ClientRoleType extends UserType { }

class EmpSupport extends EmpRoleType { }
class ClientSupport extends ClientRoleType { }

const ClassMap = {
    'EmpSupport': class EmpSupport extends EmpRoleType { },
    'ClientSupport': class ClientSupport extends ClientRoleType { }
} as const;

In this case we don't need switch anymore. Now we can define getUserType function:

type Values<T> = T[keyof T];

function hasProperty<Obj>(obj: Obj, prop: any): prop is keyof Obj {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

const withMap = <
  Key extends PropertyKey,
  Klass extends new () => any,
  ClassMap extends Record<Key, Klass>
>(
  classMap: ClassMap
) => {
  return function GetValue<Role extends PropertyKey>(
    role: Role
  ): Values<ClassMap> | null {
    if (hasProperty(classMap, role)) {
      return classMap[role];
    }

    return null;
  };
};

Now we define variable to access that function with the correct map

const GetUserType = withMap(ClassMap);

Now we can get the class type by passing a string to the function.

const userType = GetUserType('EmpSupport'); // typeof EmpSupport

Note: if a key is passed to the function it will return null

Eg. GetUserType('bad entry');

If you want to create instance of GetUserType retuen value you need to use extra parentheses

const userType = new (GetUserType('EmpSupport'))(); // EmpSupport

allowPermission should be a typeguard written as follow:

function allowParentPermission<Allowed extends typeof UserType>(
  AllowParentRole: Allowed,
  userRole: unknown
): userRole is InstanceType<Allowed> {
  return userRole instanceof AllowParentRole;
}

By convention, all classes should be capitalized, that's why I have written AllowParentRole instead of allowParentRole

Whole code:

abstract class UserType { }

abstract class EmpRoleType extends UserType { }
abstract class ClientRoleType extends UserType { }

class EmpSupport extends EmpRoleType { }
class ClientSupport extends ClientRoleType { }

const ClassMap = {
    'EmpSupport': class EmpSupport extends EmpRoleType { },
    'ClientSupport': class ClientSupport extends ClientRoleType { }
} as const;

type Values<T> = T[keyof T];

function hasProperty<Obj>(obj: Obj, prop: any): prop is keyof Obj {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

const withMap = <
  Key extends PropertyKey,
  Klass extends new () => any,
  ClassMap extends Record<Key, Klass>
>(
  classMap: ClassMap
) => {
  return function GetValue<Role extends PropertyKey>(
    role: Role
  ): Values<ClassMap> | null {
    if (hasProperty(classMap, role)) {
      return classMap[role];
    }

    return null;
  };
};

function allowParentPermission<Allowed extends typeof UserType>(
  AllowParentRole: Allowed,
  userRole: unknown
): userRole is InstanceType<Allowed> {
  return userRole instanceof AllowParentRole;
}

Playground

UPDATE

userRole is - is a special syntax for type guards

UPDATE 2

abstract class UserType { }

abstract class EmpRoleType extends UserType { }
abstract class ClientRoleType extends UserType { }

class EmpSupport extends EmpRoleType { }
class ClientSupport extends ClientRoleType { }

const ClassMap = {
    'EmpSupport': class EmpSupport extends EmpRoleType { },
    'ClientSupport': class ClientSupport extends ClientRoleType { }
} as const;

const hasProperty = <Obj,>(obj: Obj, prop: any)
    : prop is keyof Obj =>
    Object.prototype.hasOwnProperty.call(obj, prop);

type Values<T> = T[keyof T]
const withMap = <
    Key extends PropertyKey,
    Klass extends new () => any,
    ClassMap extends Record<Key, Klass>
>(classMap: ClassMap) => {
    function foo<Role extends PropertyKey>(role: Role): Role extends keyof ClassMap ? ClassMap[Role] : Values<ClassMap> | null
    function foo<Role extends PropertyKey>(role: Role): Values<ClassMap> | null {
        const storageUserType = localStorage.getItem('any item you want');

        if (hasProperty(classMap, storageUserType)) {
            return classMap[storageUserType]
        }

        return null;
    }
    return foo
}

const getUserType = withMap(ClassMap)

const foo = (str: string) => {
    const userType = getUserType(str); // EmpSupport
    if (userType) {
        const result = new userType()
    }
}

const result = getUserType('EmpSupport') //  typeof EmpSupport

Playground 2

In your function allowPermission, you are using instances of a given type, on both sides while using instanceof, but that is not how it works. instanceof is for checking if an object is of some type.

tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object. The return value is a boolean value.

Reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof

These will work,

return userRole instanceof UserType;
return userRole instanceof EmpSupport;
return userRole instanceof ClientSupport;

For your next question, regarding getUserType. Here also you are creating an instance of a type at run time, but that cannot be done without knowing what type to create an instance of, because at run time, there is no typescript.

You should probably new up the type in the method itself so that you get an instance from there instead of doing the way you have,

function getUserType(role: string): UserType | null {
  switch(role) {
    case 'EmpSupport': 
      return new EmpSupport();
    case 'ClientSupport': 
      return new ClientSupport();

    default: 
      return null;
  }
}

const x = getUserType('EmpSupport');
Related