TypeScript - pass to constructor entire object
Asked Answered
R

3

16

I have a class at type script:

export class Child {
  name:string;
  age:number;
}

I want to force class instances to have only properties that class declaration has.

For example, if I get from firebase an object:

myFirebaseService.getChild(id).then(function(child){
  var currentChild = new Child(child);
}) 

So when the object is: {name:"ben", color:"db"}, I want the result to be:

currentChild = {"name":"ben"}

Becouse "color" is not field of "child".

I tried this:

export class Child {
  name:string;
  age:number;

  constructor(tempChild:Child = null) {
    if (tempChild){
      for (var prop in tempChild) {
        this[prop] = tempChild[prop];
      }
    }
  }
}

But it is not help. "currentChild" get all of the fields and attached them to the class instance.

(Of course, I can use the following code:

export class Child {
  name:string;
  age:number;

  constructor(tempChild:Child = null) {
    if (tempChild){
      this.nam  = tempChild.name;
      this.age =tempChild.ageÏ;
    }
  }
}

, but my truth class has many fields, and I want short code)

Related answered 10/11, 2016 at 6:45 Comment(3)
I think new keyof feature of typescript 2.1 might be handy for you.Rumrunner
@AlekseyL. No, because his problem is a runtime one.Allieallied
Similar (not identical) question that may be of help - #22876136Microspore
R
16

What we want:

  • one time declare class fields
  • have methods in our class

Solution:

class Animal {
  name: string = 'default value';
  group: string = 'default value';

  constructor(data: Partial<Animal> = {}) {
    Object.assign(this, data)
  }

  echo() {
    return `My name is ${this.name}, I'm from: ${this.group}`;
  }
}

class Dog extends Animal {
  echo() {
    return super.echo() + ' from Dog class';
  }
}

const dog = new Dog({name: 'Teddy'});
console.log(dog.echo());

Animal - root class

Dog - nested class

All works without typescript errors

Rieger answered 10/11, 2019 at 12:54 Comment(2)
This solution will copy in this all the attributes of data; if we want to be sure that only the class attributes are copied, the best answer is Balázs Édes one.Faye
Note that the tsconfig option strictPropertyInitialization must be set to false for this to work.Costive
L
8

In my opinion the cleanest way to build your class instances from an arbitrary object would be using destructuring. You still have assignments for all your fields, but you have (very clean) control over what happens if the field doesn't exists, and also what fields you will assign to your classes fields:

export class Child {
  name:string
  age:number

  constructor({name = 'default name', age = 0} = {}) {
    this.name = name
    this.age = age
  }
}

Now this lets you create your instances from Objects or anys or any partial object literals, but when using literals it won't let you add extra stuff, which seems like is what you need:

const c0 = new Child({} as any /* as Object works as well */)
const c1 = new Child({}) // Literal, will be using defaults
const c2 = new Child() // No argument, using defaults only
const c3 = new Child({ name: 'a', age: 1 })
const c4 = new Child({ name: 'b', foo: 'bar'}) // error with foo

Generated js will check for everything that you would check manually:

define(["require", "exports"], function (require, exports) {
    "use strict";
    var Child = (function () {
        function Child(_a) {
            var _b = _a === void 0 ? {} : _a, _c = _b.name, name = _c === void 0 ? 'default name' : _c, _d = _b.age, age = _d === void 0 ? 0 : _d;
            this.name = name;
            this.age = age;
        }
        return Child;
    }());
    exports.Child = Child;
});

Try and see in the playground what is generated from this!

Lamrert answered 10/11, 2016 at 9:30 Comment(1)
Check this: https://mcmap.net/q/37137/-typescript-pass-to-constructor-entire-object no need to duplicate fields 3 timesRieger
A
2

You don't have many choices here, because the member definitions you make in the class before the constructor aren't being translated into javascript, the compiler leaves them out:

class Child {
    name: string;
    age: number;

    constructor() {
        this.name = "name";
    }
}

Compiles into:

var Child = (function () {
    function Child() {
        this.name = "name";
    }
    return Child;
}());

As you can see, the name member is the only one there, and even it is only used when assigned to.

Because you need the names of the members in runtime, you'll need to have them set aside, for example in an array:

class Child {
    private static MEMBERS = ["name", "age"];

    name: string;
    age: number;

    constructor(tempChild: Child = null) {
        if (tempChild) {
            for (var prop in tempChild) {
                if (Child.MEMBERS.indexOf(props) >= 0) {
                    this[prop] = tempChild[prop];
                }
            }
        }
    }
}

This isn't very comfortable to work with, so you can use decorators.
For example:

function member(cls: any, name: string) {
    if (!cls.constructor.MEMBERS) {
        cls.constructor.MEMBERS = [];
    }

    cls.constructor.MEMBERS.push(name);
}

function isMember(cls: any, name: string): boolean {
    return cls.MEMBERS[name] != null;
}

class Child {
    @member
    name: string;

    @member
    age: number;

    constructor(tempChild: Child = null) {
        if (tempChild) {
            for (var prop in tempChild) {
                if (isMember(Child, prop)) {
                    this[prop] = tempChild[prop];
                }
            }
        }
    }
}

(code in playground)

It's not tested, so I'm not sure it's working, but if you decide to go that way then it's most of what you need.

Allieallied answered 10/11, 2016 at 8:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.