0

I am new to typescript and I am trying to create a "model" class.

The constructor should accept a list of properties (that come from the database), and any of them should be optional.

Here is the code so far:

export type UserRole = "admin" | "moderator" | "user" | "visitor";


export default class User{

    public id: number | null = null;
    public login: string = '';
    public email: string = '';
    public role: UserRole = 'visitor';
    ...

    constructor({id, login, email, role, ... }){
        this.id = id;
        this.login = login;
        this.email = email;
        this.role = role;
        ....
    }

As you can see, it doesn't look right. A lot of code is duplicated. And if I want to make the properties optional it will duplicate even more code:(

Can anyone point me in the right direction? thanks

Alex
  • 66,732
  • 177
  • 439
  • 641
  • What you have done is correct as per the [typescript documentation](https://www.typescriptlang.org/docs/handbook/classes.html). Although I do agree that typing classes can feel a bit verbose. – Jack May 11 '22 at 18:01
  • 1
    Does [this approach](https://tsplay.dev/m0yGOW) meet your needs? I've written a helper function `AssignCtorWithDefaults` which accepts a default object and produces a class constructor where the constructor properties are optional but the instance it constructs has values for all properties. All you have to do is strongly type the default object. If that works for you I can write up an answer explaining how it works. If it doesn't work for you, what am I missing? – jcalz May 11 '22 at 18:49
  • looks good jcalz, why don't you post it as an answer? – Alex May 11 '22 at 19:17

2 Answers2

2

I would suggest to use following utility type from here:

type NonFunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? never : K
}[keyof T];

type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

This will create a type out of all properties of a class without the methods.

You can use it like this:


export type UserRole = "admin" | "moderator" | "user" | "visitor";


export default class User{

    public id: number | null = null;
    public login: string = '';
    public email: string = '';
    public role: UserRole = 'visitor';
    

    constructor({id, login, email, role }: NonFunctionProperties<User>){
        this.id = id;
        this.login = login;
        this.email = email;
        this.role = role
    }
}

To make them all optional, just add Partial:

constructor({id, login, email, role }: Partial<NonFunctionProperties<User>>)

Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
2

Here's how I would implement the User class.

type UserRole = 'admin' | 'moderator' | 'user' | 'visitor';

class User {
    constructor(
        public id: number | null = null,
        public login: string = '',
        public email: string = '',
        public role: UserRole = 'visitor'
    ) {}
}

However, if you want to create the instance of the User class from an object of user properties then you will require some duplication.

type UserRole = 'admin' | 'moderator' | 'user' | 'visitor';

interface UserProps {
    id: number | null;
    login: string;
    email: string;
    role: UserRole;
}

class User implements UserProps {
    constructor(
        public id: number | null = null,
        public login: string = '',
        public email: string = '',
        public role: UserRole = 'visitor'
    ) {}

    static fromProps({ id, login, email, role }: Partial<UserProps> = {}) {
        return new User(id, login, email, role);
    }
}

There's no elegant way around this duplication. Tobias S. and jcalz show hacks to avoid this duplication, but I would discourage you from using such hacks.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299