I'm trying to create a JavaScript class which takes an object as its only argument. The object properties shall be merged with some defaults which are defined in the class itself (and then be used as class fields). So far I'm using object destructuring to achieve what I want to do – which works great when the object is only one level deep. As soon as I use a nested object I'm not able to overwrite "parent" properties (without passing a nested object) anymore.
Is object destructuring even the right approach to what I want to do?
The class itself looks like this
class Class {
constructor( args = {} ) {
this.prop1 = {};
( {
type: this.type = 'default',
prop1: {
value1: this.prop1.value1 = 'one',
value2: this.prop1.value2 = 'two',
value3: this.prop1.value3 = 'three'
} = this.prop1
} = args );
}
}
Expected
When creating a new Class with the following call
new Class( { type: 'myclass', prop1: { value1: 1 } } );
the class fields are assigned properly and the class has the following structure:
{
type: 'myclass',
prop1: {
value1: 1,
value2: 'two',
value3: 'three'
}
}
Which is perfect and exactly what I want to achieve.
Unexpected
It gets tricky when I want to overwrite some fields not with the expected values / structure. A new class creation like the following "fails" as the input values are overwritten with the default values.
The following call
new Class( { type: 'myclass', prop1: 'myProp1' } );
results in the following structure:
{
type: 'myclass',
prop1: {
value1: 'one',
value2: 'two',
value3: 'three'
}
}
Which is not what I want it to be / what I expected.
I would have expected the following structure:
{
type: 'myclass',
prop1: 'myProp1'
}
Is there any way to generate the class in the described way – using object destructuring – or is it just not the right use case?
Thanks in advance!
I would avoid having a property that can be either a string or an object, but if you really need it (not just as input but also as the resulting class property), you can achieve this by destructuring twice:
class Class {
constructor( args = {} ) {
( {
type: this.type = 'default',
prop1: this.prop1 = {},
} = args );
if (typeof this.prop1 == 'object') {
( {
value1: this.prop1.value1 = 'one',
value2: this.prop1.value2 = 'two',
value3: this.prop1.value3 = 'three',
} = this.prop1 );
}
}
}
(Notice that this does mutate the args.prop1 if you pass an object!)
I'd avoid destructuring onto object properties though, it's fairly uncommon (unknown) and doesn't look very nice. I'd rather write
class Class {
constructor(args) {
this.type = args?.type ?? 'default',
this.prop1 = args?.prop1 ?? {};
if (typeof this.prop1 == 'object') {
this.prop1.value1 ??= 'one',
this.prop1.value2 ??= 'two',
this.prop1.value3 ??= 'three',
}
}
}
(This still does mutate the args.prop1 if you pass an object! Also it treats null values differently)
or if you really want to use destructuring,
class Class {
constructor({type = 'default', prop1 = {}} = {}) {
this.type = type;
if (typeof prop1 == 'object') {
const {value1 = 'one', value2 = 'two', value3 = 'three'} = prop1;
this.prop1 = {value1, value2, value3};
} else {
this.prop1 = prop1;
}
}
}
(This does always create a new this.prop1 object that is distinct from args.prop1)
If I was reviewing your code, I would make you change it. I've never seen destructuring used like that to set values on another object. It's very strange and smelly... but I see what you're trying to do.
In the most simple of cases, I suggest using Object.assign in this scenario. This allows you to merge multiple objects in the way you want (notice how easy it is to read):
const DEFAULTS = {
type: 'default',
prop1: {
value1: 'one',
value2: 'two',
value3: 'three'
}
}
class SomeClass {
constructor( args = {} ) {
Object.assign(this, DEFAULTS, args);
}
}
This will only merge top-level properties. If you want to merge deeply nested objects too, you will need to do that by hand, or use a tool like deepmerge. Here's an example of doing it by hand (while less easy to read, it's still pretty normal code):
class SomeClass {
constructor( args = {} ) {
Object.assign(this, {
...DEFAULTS,
...args,
prop1: typeof args.prop1 === 'string' ? args.prop1 : {
...DEFAULTS.prop1,
...args.prop1
}
});
}
}
Related
I have an empty javascript object, and I want to do something like this:
this.conflictDetails[this.instanceId][this.modelName][this.entityId] = { solved: true };
The problem is that for the modelName I get Cannot read properties of undefined.
I think it is because the empty object this.conflictDetails does not have the property that I m looking for, I wonder how I could write this to work and to be as clear as possible?
The result should look like:
{
'someInstanceId': {
'someModelName': {
'some entityId': {
solved: true;
}
}
}
}
With mu; time instance ids, that hold multiple model names, that have multiple entity Ids
check this the '?' operator
let obj = {
'someInstanceId': {
'someModelName': {
'someEntityId': {
solved: true
}
}
}
};
let value1 = obj.someInstanceId?.someModelName?.someEntityId;
console.log(value1);
// or
value1 = obj["someInstanceId"] ?? {};
value1 = value1["someModelName"] ?? {};
value1 = value1["someEntityId"];
console.log(value1);
let value2 = obj.someInstanceId?.someMissModelName?.someEntityId;
console.log(value2);
You can use interfaces to define the model of the object, or you can check if every nested object is not null nor undefined.
I would prefer the first option if I use typescript. If not, then i would stick to the not null/undefined option
First we create a model:
export interface CustomObject {
[instanceId: string]: {
[modelName: string]: {
[entityId: string]: {
solved: boolean;
};
};
};
}
Now that we have a model we can fill an object with data:
We define the object in the .ts file of your component
public dataObjects: CustomObject = {
instance1: {
basicModel: {
Entity1: {
solved: false,
},
},
}
};
Now you can have for example a function to change the solved status, I assume that there are several instanceIds, modelNames and entityIds.
public resolveObject(
instanceId: string,
modelName: string,
entity: string,
solved: boolean
) {
this.dataObjects[instanceId][modelName][entity].solved = solved;
}
Now you can call the method with the object details:
this.resolveObject('instance1', 'basicModel', 'Entity1', true);
After this line is executed the solved will changer from false to true.
I've created a StackBlitz to show you: https://stackblitz.com/edit/angular-ivy-ibfohk
Okay so I know the title can be a bit confusing, so let me explain.
I have the following interfaces and an array of Items[]:
interface Items {
step: number;
keyName: string;
value: string;
}
interface ReturnData {
foo: string;
bar: string;
baz: string;
}
const items: Items[] = [
{ step: 1, keyName: 'foo', value: 'fooVal' },
{ step: 2, keyName: 'bar', value: 'barVal' },
{ step: 3, keyName: 'baz', value: 'bazVal' },
];
Now imagine I want function that accepts Items[] as a parameter, loops over the items, and for each item assigns a new property with a key of keyName and a value of value to a new object of type ReturnData, and then it finally returns that object as a promise.
For demonstration purposes I am using setInterval here, but the same would work with a for loop or other method.
const myFunc = (items: Items[]): Promise<ReturnData> => {
let index = 0;
let returnData: ReturnData = { foo: '', bar: '', baz: '' };
return new Promise(resolve => {
const interval = setInterval(() => {
/*
at index 0: items[index].keyName = 'foo' and items[index].value = 'fooVal'
at index 1: items[index].keyName = 'bar' and items[index].value = 'barVal'
at index 2: items[index].keyName = 'baz' and items[index].value = 'bazVal'
*/
returnData[items[index].keyName] = items[index]?.value;
if (items[index]?.step === items.length) {
clearInterval(interval);
resolve(returnData);
}
else {
index += 1;
}
}, 250);
})
}
(async () => {
const myResult = await myFunc(items);
console.log('myResult value is:', myResult);
/* myResult value is: { foo: 'fooVal', bar: 'barVal', baz: 'bazVal' } */
})();
In plain javascript this function would run properly and return myResult as expected. But in Typescript I am getting the following error messages when I try to dynamically assign properties to returnData at each iteration of my setInterval.
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ReturnData'.
No index signature with a parameter of type 'string' was found on type 'ReturnData'.ts(7053)
and
Object is possibly 'undefined'.ts(2532)
I have recently started learning Typescript, so my question is, how do I make Typescript understand what is happening here so that it doesn't throw the an error. I know a quick and dirty trick is to assign type any to my returnData object to get rid of the errors, but that wouldn't be exactly "type-safe" if that is the correct terminology. What is a better way to go about and make typescript happy?
Full code example at TS Playground
Updating this line should solve your issue:
Issue is that typescript can not associate they keyName of string type as being a key of ReturnData.
interface Items {
step: number;
keyName: keyof ReturnData; // only if you expect that ReturnData keyName will always belong to ReturnData
value: string;
}
typescript
or
returnData[(items[index]!.keyName) as keyof ReturnData] = items[index]?.value ?? '';
Typescript playground
Just change your interface like this, using keyof operator.
interface Items {
step: number;
keyName: keyof ReturnData;
value: string;
}
keyName is now: "foo" | "bar" | "baz"
I'm using an existing 3rd party API over which I have no control and which requires me to pass an array with an additional property. Something like the following:
type SomeArgument = string[] & { foo: string };
doSomething (argument: SomeArgument);
Now I find it quite clumsy and verbose, to create SomeArgument while keeping the compiler satisfied.
This one works, but no type safety at all:
const customArray: any = ['baz'];
customArray.foo = 'bar';
doSomething(customArray);
Another option which feels cleaner, but is quite verbose (I will need different subclasses with different properties) is to subclass Array:
class SomeArgumentImpl extends Array<string> {
constructor (public foo: string, content?: Array<string>) {
super(...content);
}
}
doSomething(new SomeArgumentImpl('bar', ['baz']));
Is there any better, one-liner-style way? I was hoping for something along doSomething({ ...['baz'], foo: 'bar' }); (this one does not work, obviously).
Suppose this is the function you want to call:
function doSomething (argument: string[] & { foo: string }) {
argument.push("a");
argument.foo = "4";
console.log(argument);
}
Then you can call it like this:
// Works
doSomething(Object.assign(["a", "b"], { foo: "c" }));
// Error
doSomething(Object.assign(["a", 2], { foo: "c" }));
// Error
doSomething(Object.assign(["a", 2], { foo: 4 }));
I'm pretty new with these languages and I'm sure it's not so complicated but could not find how to walk through class instance variables dynamically.
I'm trying to display instance's variables in a table using Angular. I'd like to write a single function that will work with any class.
Let's say I have a workbook class:
export class workbookEntity {
public name: string;
public creationDate: string;
constructor() {
this.name = 'my work book name';
this.creationDate = '2018/01/26';
}
}
And let's say I want to get the names and values of the variables of an instance of this class in another class' function:
export class showClassComponent {
// some code here
whenSubmitedInHtmlForm(className: string): void {
// do something to walk through instance
// with className parameter = 'workbookEntity'
}
// more code there
}
How would you walk through the instance to get each variable's name and value to get something like this?
[
{
name: 'name',
value: 'my work book name'
},
{
name: 'creationDate',
value: '2018/01/26'
}
]
There's no concept of reflection, so much in Typescript, so you can't neccessarily do a type lookup. But you might be able to do something along the lines of...
export class showClassComponent {
var classes = [ { name: 'workBookEntity', value: new WorkBookEntity() }]
whenSubmitedInHtmlForm(className: string): void {
let c = classes.filter(class => class.name == className)[0];
if(c) {
let ret = [];
for (var key in c) {
if (c.hasOwnProperty(key)) {
ret.push({ name: key, value: c[key] });
}
}
return ret;
}
}
// more code there
}
If you want to be able to retain accurate type information, you can create a pick utility function and then just import it into your class module wherever you need to use it.
let workBook = new WorkbookEntity();
export function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
let newObj = {} as Pick<T, K>;
for (const key of keys) {
newObj[key] = obj[key];
}
return newObj;
}
console.log(pick(workBook, ['name', 'creationDate']));
// log: { name: 'my work book name', creationDate: '2018/01/26' }
Then you can import the first function into another utility function if you to be able to handle multiple objects.
export function pickObjects<T extends object, K extends keyof T>
(objects: T[], keys: K[]): Pick<T, K>[] {
return objects.reduce((result, obj) => [...result, pick(obj, keys)], []);
}
let workbooks = pickObjects([workBook, workBook2], ['name']);
workbooks[0].name // this exists
workbooks[0].age // Typescript error.
The editor will show that age doesn't exist since we didn't pick it
I'm trying to default options in ES7 using babel. Here is what I can do:
class Foo {
constructor({key='value', foo='bar', answer=42}) {
this.key = key;
this.foo = foo;
this.number = number;
}
}
This might work for this example, but I would like to know how can I assign for very large config objects; here is an example of what I wanna do:
class Foo {
constructor(opts = {key='value', foo='bar', answer=42}) {
this.opts = opts;
}
}
However this does not compile. I tried to do it like this:
class Foo {
constructor(opts = {key:'value', foo:'bar', answer:42}) {
this.opts = opts;
}
}
But then it replaces the whole object, like this:
let foo = new Foo({key: 'foobar'});
console.log(foo.opts);
// {key: 'foobar'} is what is displayed
// When I want {key: 'foobar', foo:'bar', answer:42}
I don't think you can do this with ES6 optional parameters (object as a parameter with optional keys), because when you call the constructor, it's a new object with a new reference. That's because it's being replaced.
But, as a suggestion, if you want to handle a large options object, one common approach is store somewhere a default options Object and merge the object with the one passed when you instantiate it.
Something like that:
class Foo {
constructor(opts) {
this.opts = Object.assign({}, Foo.defaultOptions, opts)
console.log(this.opts)
}
}
Foo.defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
let foo = new Foo({key: 'another value'})
//{ key: 'another value', foo: 'bar', answer: 42 }
You can merge with Object.assign (be aware that it does not perform deep merging - nested objects are replaced).
Or, if you want to declare your default options Object as a class variable (not at the end, after class declaration, or inside constructor), as you're using babel, you can use this plugin and do this:
class Foo {
defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
constructor(opts) {
this.opts = Object.assign({}, this.defaultOptions, opts)
console.log(this.opts)
}
}
It's more readable.
It is
class Foo {
constructor({key='value', foo='bar', answer=42} = {}) {
...
}
}
It is ES6 destructuring feature, not specific to ECMAScript 7 (ECMAScript Next) proposals.
Without destructuring it is usually done with object cloning/merging, Object.assign comes to help:
class Foo {
constructor(opts = {}) {
this.opts = Object.assign({
key: 'value',
foo: 'bar',
answer: 42
}, opts);
}
}