Digital Ninja
Digital Ninja

Reputation: 3731

Object with arbitrary properties as class property

I am just starting out with Typescript, and I cannot understand if it's possible to have a class property be an object, which would hold any arbitrary property in addition to properties declared in the class. For example, I defined here a name as a property of a Person, and then under properties you should be able to define any other arbitrary characteristics for the person, such as their height. It seems that assigning it goes okay, but trying to access it on line 12 throws an error saying:

Property 'height' does not exist on type 'Object'

Fair enough! I know there is no guarantee that a property named height would be under something that is just an object, but there should still be a way to do this.

Here is the code:

class Person {
    public name: string;
    public properties: Object; 
    constructor(name: string, other: Object) {
        this.name = name;
        this.properties = other;
    }
}

let props: Object = { height: 200 };
var nick = new Person("Bob", props);
console.log(nick.properties.height);

And here is an alternative I've tried, which throws exactly the same error:

class Person {
    public name: string;
    public properties: Object; 
    constructor(name: string, other:{ height: number }) {
        this.name = name;
        this.properties = other;
    }
}


var nick = new Person("Bob", { height: 200 });
console.log(nick.properties.height);

Another alternative with an interface I've just tried, that still doesn't work.

interface PersonProperties {
    height: number;
}

class Person {
    public name: string;
    public properties: Object; 
    constructor(name: string, other: PersonProperties) {
        this.name = name;
        this.properties = other;
    }
    getHeight(): number {
        return this.properties.height;
    }
}

var properties: PersonProperties = { height: 200 }; 
var nick = new Person("Bob", properties);
document.write(nick.getHeight().toString());

Upvotes: 1

Views: 12683

Answers (3)

Sergey Tityenok
Sergey Tityenok

Reputation: 31

You can use type mapping. Create type that consists of all keys (Wrap) and class which should map the properties you want to work with (UserIdentifier) and than mix that in parameter of your function (constructor of User object in my example)

type Wrap<T> = {
    [P in keyof T]?: T[P]
}

type UserIdentifier = {
    userId: string;
}

class User {
    constructor(user: Wrap<any> & UserIdentifier) {
        this.user = user;
    }
    user: Wrap<any> & UserIdentifier;

    userId(): string {
        return this.user.userId;
    }

}

//then can be called as 
let u = new User({userId:"usr1", someOther:1, andOther:2, andSoOn:3});
//or just 
let u2 = new User({userId:"usr1"});
//however this will throw an error because required userId property is not present
let u3 = new User({someOther:1});

Upvotes: 0

martin
martin

Reputation: 96891

The error happens because you're defining public properties: Object; and Object really doesn't have a property height. Even though you declare correct type in the constructor with { height: number } property properties is still expected to be an Object.

You could do for example this:

type PropertiesObject = { height: number };

class Person {
    public name: string;
    public properties: PropertiesObject; 
    constructor(name: string, other: PropertiesObject) {
        this.name = name;
        this.properties = other;
    }
}

let props = <PropertiesObject>{ height: 200 };
var nick = new Person("Bob", props);
console.log(nick.properties.height);

Using interface as you did is correct as well.

See live demo

Note that you can always use good old square brackets notation. This should compile successfully even when using just Object:

console.log(nick.properties['height']);

Upvotes: 2

Bal&#225;zs &#201;des
Bal&#225;zs &#201;des

Reputation: 13807

Since the static type of Person#properties is simply Object the type checker doesn't keep any additional type information about it, that's why you get the compile errors. You can solve this issue 2 ways

The "dumb" way with any:

class Person {
  constructor(public name: string, public other: any) {}
  /* ... */
}

const p = new Person("doge", { wow : "such property" })
console.log(p.other.wow) // no error, also no type checking

any in ts basically "disables" typechecking, and lets you access any properties on a variable typed as any

The slightly smarter way with generics

class Person<PropType> {
  constructor(public name: string, public other: PropType) {}
}

const p = new Person("doge", { wow : "such property" })
console.log(p.other.wow) // ok
console.log(p.other.amaze) // error

This way each person instance will have an associated properties type, so it's checked "compile" time if the property you trying to access is known by the compiler.

I'd recommend some reading on generics if this doesn't look familiar from other languages: https://www.typescriptlang.org/docs/handbook/generics.html

Upvotes: 6

Related Questions