How to build a TypeScript class constructor with object defining class fields?

The simplest way would be to declare the fields in the class and use a mapped type as a parameter, then use Object.assign to assign the fields to this. We have several options for which mapped type to use:

Partial<T>

Type will contain all members (fields and methods) of the class but all of them are optional. The disadvantage here is we can’t make some of the fields required, and the caller can potentially override a method

class Item {

    public id: number;
    public updatedAt: number;
    public createdAt: number;
    constructor(data: Partial<Item>) {
        Object.assign(this, data);
    }
    method() {}
}

//Works 
const item = new Item({ id: 1, updatedAt: 1, createdAt: 1 });
//This also works unfortunately 
const item2 = new Item({ id: 1, method() { console.log('overriden from param !')} });

Pick<T, K>

This mapped type allows us to pick some properties from T by specifying a union of several string literal types that are keys of T. The advantages are that Pick will inherit whether the field is required or not from the original declaration in the class (so some fields can be required and other optional) and since we specify which members we pick, we can omit methods. The disadvantage is that we have to write property names twice (once in the class and once in the Pick):

class Item {
    public id: number;
    public updatedAt?: number;
    public createdAt?: number;
    constructor(data: Pick<Item, "id" | "updatedAt" | "createdAt">) {
        Object.assign(this, data);
    }
    method() {}
}
const item = new Item({ id: 1  }); //id is required others fields are not
const item2 = new Item({ id: 1, method() {}  }); // error method is not allowed

Custom Mapped Type That Removes Methods

The third option would be create a type similar to Pick that includes all class fields but not the methods automatically. We can do this in Typescript 2.8 using conditional types (unrelease at the time of writing, but should be release in March 2018, you can get it right now via npm install -g typescript@next). This has the advantages of Pick without the need to specify filed names again:

type NonMethodKeys<T> = {[P in keyof T]: T[P] extends Function ? never : P }[keyof T];  
type RemoveMethods<T> = Pick<T, NonMethodKeys<T>>; 

class Item {
    public id!: number;
    public updatedAt?: number;
    public createdAt?: number;
    constructor(data: RemoveMethods<Item>) { // No need to specify field names again
        Object.assign(this, data);
    }
    method() {}
}

const item = new Item({ id: 1  });  //id is required others fields are not
const item2 = new Item({ id: 1, method() {}  }); // error method is not allowed 

Playground Link

Leave a Comment