Reputation: 1088
I am currently in the process of converting come legacy code into typescript for a project. The project contains a number of custom classes written in pure javascript which follow the same pattern of having a constructor which takes in a config object which can have many configuration options defined within it.
I've been toying with how to define the allowed config properties in typescript and haven't yet come up with a solution I'm entirely happy with. I started by declaring an interface for the config, which allows me to define the config options available but doesn't help with defaulting them - I have to copy my config interface twice or more for both the definition and the initialisation. What I (think I) want is to use object destrcuturing to define the class members once with default values and not have to worry about it after that e.g:
export interface MyClassConfig {
propA: string;
propB: boolean;
}
export class MyClass {
private propA: string = 'defautValue';
private propB: boolean;
constructor (config: MyClassConfig) {
{ propA, propB } = config;
}
}
Which could then be initialised with something like:
let mc = new MyClass({ propB: false }); //mc.propA == 'defaultValue', mc.propB === false
I don't like that this requires me to copy the property names multiple times though. Can anyone suggest a nice clean way to achieve this?
Upvotes: 9
Views: 7228
Reputation: 1862
I was facing the same problem: Destructuring and assigning to class members.
I've found the following solution that meets my needs and maybe yours:
interface MyClassConfig {
propA?: string; // Note the '?' for optional property
propB: boolean;
}
class MyClass {
private propA: string = 'defautValue';
private propB: boolean;
constructor (config: MyClassConfig) {
({ propA: this.propA = this.propA, propB: this.propB } = config);
}
}
const myClass = new MyClass({ propA: 'aaa', propB: true });
console.log(myClass)
// MyClass:
// propA = 'aaa';
// propB = true;
const myClass2 = new MyClass({ propB: false });
console.log(myClass2)
// MyClass:
// propA = 'defaultValue';
// propB = false;
See MDN Destructuring assignment chapter "Assigning to new variables names and providing default values".
See JSBin for a working sample
Upvotes: 4
Reputation: 3199
I don't see the big issue of adding the config object as a member object to your class. All the current solutions with the help of the latest Typescript version aren't nice and just over complicates something as simple as this. Something similar has been suggested before Combining destructuring with parameter properties. This proposal is nearly 2 years old. However, there are numerous ways to do the above, something I am not certain about is whether or not you need to access the fields inside your class as well.
Onto some methods that might help you solve the above, given the following interface...
interface IConfigOpts {
propA: string,
propB: boolean
propC: number
}
...and this class structure.
class Config {
constructor(opts: IConfigOpts) {
// Object.assign(this, opts);
Object.entries(opts).forEach((property: [string, any]) => {
Object.defineProperty(this, property[0], {
writable: true,
value: property[1]
});
});
}
get props() {
return Object.getOwnPropertyNames(this).map(key => ({[key]: this[key]}));
}
}
You can use Object.assign
(es5+), as suggested by Frank, or Object.defineProperty
to initialize the member fields in the constructor. The latter gives the option to make the properties writable
or assign getters
and/or setters
.
You can create a factory method to get the properties with their types outside of the class using the creation of a new instance of your config class and the interface:
const ConfigFactory = (opts: IConfigOpts) => new Config(opts) as Config & IConfigOpts;
const cfg = ConfigFactory({
propA: 'propA',
propB: true,
propC: 5
});
let a: string = cfg.propA; // OK => defined
But this won't allow you to use this.propA
in the class with type checking. You cant (yet) dynamically bind typed class members to a class using destructures. And forces you to use this[key]
instead.
If you don't want to access the properties outside of the class, you can define a method to call properties by their name and use their types to validate the defined types in the given interface this omits the use of this[key]
and returns their given type:
prop<TKey extends keyof IConfigOpts, TValue extends IConfigOpts[TKey]>(key: TKey): TValue {
return this[key as string];
}
This ensures the entered key exists in the in the interface using a lookup with keyof
and returns the correct type as well.
let a: string = cfg.prop('propA'); // OK => defined
let b: boolean = cfg.prop('propB'); // OK => defined
let c: number = cfg.prop('propC'); // OK => defined
let d: number = cfg.prop('propD'); // Fail => Argument is not assignable to keyof IConfigOpts
let e: boolean = cfg.prop('propC'); // Fail => Type number is not assignle to type boolean
The only nice thing about automatically binding (lots of) properties to a class is that you do not need to edit the destructure of the object in the given constructor to match the keys of the object. e.g. propA
changes to propAB
. All you would have to change is the interface declaration.
Upvotes: 2
Reputation: 10516
I don't think you can use destructuring to reference properties of this
in the constructor. How about using Object.assign
to copy properties from the config over to the instance?
constructor (config: MyClassConfig) {
Object.assign(this, config);
}
Note that this will make shallow copies of reference types. Also note that if any of the property names of MyClass
change, you'll have to remember to change the property names in MyClassConfig
and vice versa.
Upvotes: 2