Reputation: 100312
In TypeScript it's possible to create a class with a constructor that takes parameter with access modifiers and it automatically convert those parameters in class fields.
class Item {
constructor(
public id: number,
public updatedAt: number,
public createdAt: number,
) {}
}
const item = new Item(1, 1, 1);
item.id // 1
I'm wondering if there is a way to pass all those parameters in an object instead
class Item {
constructor({
public id: number,
public updatedAt: number,
public createdAt: number,
}) {}
}
const item = new Item({ id: 1, updatedAt: 1, createdAt: 1 });
item.id // 1
Is this possible? Ever gonna be possible?
Are there workarounds to do something similar?
Upvotes: 44
Views: 21229
Reputation: 3931
This is the best that I've been able to find. It allows optional constructor parameters elegantly, and default values, albeit considerably less elegantly. Also, the export
needs to be on its own line.
interface ItemParams {
id: number;
updatedAt?: number;
createdAt: number;
}
interface Item extends ItemParams { };
class Item {
constructor(params: ItemParams) {
// The generic types must be set, or type checking will not be performed.
Object.assign<this, ItemParams>(this, {
// Somewhat hacky default values
updatedAt: new Date().getTime(),
...params
});
}
};
export { Item };
Usage
const item = new Item({ id: 1, createdAt: 1 });
console.log(JSON.stringify(item, null, 4));
Output
{
"updatedAt": 1719091079502,
"id": 1,
"createdAt": 1
}
Upvotes: 1
Reputation: 2352
Building on Titian's answer, if you don't want to copy paste the RemoveMethods
type, then you can achieve this with type-fest
's ConditionalExcept
:
import { ConditionalExcept } from 'type-fest';
class Item {
required_property!: int;
optional_property?: string;
constructor(object: ConditionalExcept<Item, Function>) {
Object.assign(this, object);
}
method() {}
}
Upvotes: 0
Reputation: 249466
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
Upvotes: 52
Reputation: 2865
You can achieve similar behavior by creating an interface:
interface IProps {
id: number;
updatedAt: number;
createdAt: number;
}
class Item {
constructor(public props: IProps) {}
}
const item = new Item({ id: 1, updatedAt: 1, createdAt: 1 });
console.log(item.props.id);
Upvotes: 1