twoLeftFeet
twoLeftFeet

Reputation: 720

Nesting class structures gives compiler errors

I have a class hierarchy like this:

User:

Role:

Each class has a constructor that converts the args into its designated class, for instance User converts roles into Role instances. Role converts permission into Permission instance.

Sample code:

const user = new User({
        roles: [
            {role_name: 'admin', permissions: {'*': '*'}} 
        ]
    })

When I pass JSON for user, with the nested permission object, I get compiler errors, because Permission has methods on it. Property 'has' is missing in type '{ '*': string; }' but required in type 'Permission'.

How can I avoid these issues?

Edit:

How can I enable the Roles class to accept the permissions as an object so that it can correctly coerce the JSON to the Permission type?

// users.ts
export interface User extends BaseModel {
    createdAt?: string;
    updatedAt?: string;
    email?: string;
    username?: string;
    fullname?: string;
    microsoftId?: string;
    roles?: UserRole[];
}

export class User {
    constructor(props?: Partial<User>) {
        if(!props){
            props = {};
        }

        props.roles = props.roles?.map(role => new Role(role)) || [];
        
        Object.assign(this, props);
    }

    hasPermission(service: string, perm: PermissionType){
        for(let role of this.roles || []){
            const has = (role.permissions as Permission).has(service, perm);
            if(has){
                return has;
            }
        }

        return false;
    }
}

// roles.ts


export interface Permission {
    [key: string]: PermissionType[]|any;
}

export class Permission {
    constructor(props: Partial<Permission>){
        Object.assign(this, props);
    }

    has(service: keyof Permission, permName: PermissionType|undefined): boolean {
        //.....
    }
}
export interface Role extends BaseModel {
    createdAt?: string;
    updatedAt?: string;
    role_name?: string;
    description?: string;
    scopes?: string[];
    permissions: Permission;
}
export class Role {
    constructor(props?: Partial<Role>) {
        if(!props){
            props = {};
        }

        props.permissions = new Permission(props.permissions || {});
        
        if (!Array.isArray(props.scopes)) {
            props.scopes = [];
        }

        Object.assign(this, props);
    }
}

Upvotes: 0

Views: 33

Answers (1)

Silvio Mayolo
Silvio Mayolo

Reputation: 70297

Don't name the interface the same as the class. You're merging the names and creating a frankly rather awkward setup. Have a separate interface for constructor arguments.

export interface PlainPermission {
    [key: string]: PermissionType[]|any;
}

export class Permission implements PlainPermission {
    [key: string]: PermissionType[]|any;

    constructor(props: Partial<PlainPermission>){
        Object.assign(this, props);
    }

    has(service: keyof Permission, permName: PermissionType|undefined): boolean {
        //.....
    }
}

and the same for Role and User. Then you have a clear distinction: PlainPermission is the "plain" Javascript object you pass to constructors and Permission is the thing with all the nice, convenient methods attached to it.

When you define an interface and a class with the same name, they get merged to create one larger class, and Permission is defined to be "anything that has the stuff from the interface and a has method", which is not what you want.

You're going to have to duplicate the field names from the interface in the class, but that makes sense: an interface is a promise to implement something, whereas a class is the actual implementation. One is a contract, the other does the work stated in the contract. You can use implements PlainPermission as an extra check to make sure you implemented the interface correctly.

Upvotes: 1

Related Questions