Reputation: 2700
Currently I am defining the constructor args and instance properties of my Typescript classes like this:
interface ConstructorProps {
foo: number;
bar: string;
baz: boolean;
}
class MyClass {
foo: number;
bar: string;
baz: boolean;
constructor (props: ConstructorProps) {
this.foo = props.foo;
this.bar = props.bar;
this.baz = props.baz;
}
}
Which is quite repetitive. Is there a way of doing this in a cleaner way, even if it just for the interface and instance properties?
Upvotes: 2
Views: 111
Reputation: 249466
Using mapped types and conditional types we can create a type that contains all the fields of a class and use it as a constructor argument. Also we can use Object.assign
to copy the properties instead of assigning each field:
type NotMethods<T> = { [P in keyof T]: T[P] extends (...args: any[]) => any ? never: P }[keyof T];
type Fields<T> = { [P in NotMethods<T>]: T[P] }
class MyClass {
foo: number;
bar: string;
baz: boolean;
constructor(props: Fields<MyClass>) {
Object.assign(this, props)
}
method() {
}
}
new MyClass({
foo: 1,
bar: '',
baz: true
});
Playground link
The Fields
type is reusable for any class. All fields of the class will all be required in the constructor argument.
Or if you have optional fields and are using strict null checks, you can write a version that preserves the optionality of the field in the constructor parameter:
type NotMethods<T> = Exclude<{ [P in keyof T]: T[P] extends (...args: any[]) => any ? never: P }[keyof T], undefined>;
type FilterUndefined<T, TKeys extends keyof T = keyof T> = Exclude<{ [P in TKeys]: undefined extends T[P] ? never: P }[TKeys], undefined>;
type KeepUndefined<T, TKeys extends keyof T = keyof T> = Exclude<TKeys, FilterUndefined<T, TKeys> | undefined>
type Fields<T> = { [P in FilterUndefined<T, NotMethods<T>>]: T[P] } & { [P in KeepUndefined<T, NotMethods<T>>]?: T[P] }
class MyClass {
foo: number;
bar: string;
baz?: boolean;
constructor(props: Fields<MyClass>) {
Object.assign(this, props)
props.foo
}
method() {
}
}
new MyClass({
foo: 1,
bar: '',
baz: true
});
new MyClass({
foo: 1,
bar: '',
});
Playground link
Upvotes: 1
Reputation: 12691
You can also consider the builder pattern :
type Builder<T> = {
[k in keyof T]: (arg: T[k]) => Builder<T>
} & { build(): T }
function createBuilder<T>(type: { new(...args: any[]) : T}): Builder<T> {
var built: any = {};
var builder = new Proxy({}, {
get: function(target, prop, receiver) {
if (prop === 'build') {
return () => new type(built);
}
return (x: any): any => {
built[prop] = x;
return builder;
}
}
});
return builder as any;
}
class MyClass {
foo: number;
bar: string;
baz: boolean;
public static createBuilder(): Builder<MyClass> {
return createBuilder<MyClass>(MyClass);
}
constructor (builder: Builder<MyClass>) {
Object.assign(this, builder);
}
}
const me = MyClass.createBuilder()
.bar("bar")
.baz(true)
.foo(3)
.build();
console.log(me);
Upvotes: 0
Reputation: 9151
This might not work for all scenarios, but this compiles:
class MyClass {
foo: number;
bar: string;
baz: boolean;
constructor (props: MyClass) {
this.foo = props.foo;
this.bar = props.bar;
this.baz = props.baz;
}
}
const props = {
foo: 1,
bar: "s",
baz: true
};
const x = new MyClass(props);
Upvotes: 0