Reputation: 720
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
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