Dynamically Convert Plain Objects Into TypeScript Classes

Tags: typescript, javascript, webdev
Published: 2020-07-24 21:07:22

Dealing with plain objects (or the result of JSON.parse) is a fundamental part of web development. In order to make the development experience bearable, we often shape plain objects into something predictable, including transforming their values into something more powerful than primitive types.

There are several approaches we can take. One is factory functions, which require you to define the transform function, plus an interface with its type definition. Another, classes, which are self-contained in the sense of functionality and type. I’d like to make a case for the latter since classes don’t get much love these days.

We will explore what it would take to stick plain objects into classes while allowing us the flexibility we need when working with our custom data types and providing additional functionality (getters/setters/methods).

# The Simple Solution
To start, we’ll define a plain class:
```typescript
class Person {
  name: string
  lastName: string
  
  constructor(payload: Partial<Person>){
    this.name = payload.name || ''
    this.lastName = payload.lastName || ''
  }
}
```

Which we can then use like this:
```javascript
new Person({ name: 'Alice', lastName: 'Liddell' });

// and since the payload can be a "Partial" Person
new Person({ name: 'Alice' });
```
But we want more. We want to be able to construct these dynamically at runtime, from incoming values, without having to go the constructor of each class to set up each property.

An alternative would be to programmatically assign the values to each property from the payload. A common pattern for this is the following:
```javascript
class Person {
  name: string = '';
  lastName: string = '';

  constructor(payload: Partial<Person>){
    for(const key in payload){
      if(this.hasOwnProperty(key)){
        this[key] = payload[key];
      }
    }
  }
}
```

It’s pretty straightforward: We call `this.hasOwnProperty` to make sure we set values only for properties belonging to this class.

This is good and all, but it will only be useful when we want our class to only contain primitive types. Aside from that is the fact that we’d need to repeat the same constructor in every class.

Let’s see a more practical class definition:
```typescript
import { DateTime } from 'luxon'
import { Decimal } from 'decimal.js'

class Address {
  no: string = ''
  street: string = ''
  city: string = ''
}

class Person {
  name: string = ''
  lastName: string = ''
  dob: DateTime = DateTime.local()
  address: Address = new Address()
  netWorth: Decimal = new Decimal(0)
}
```

This is closer to what we’d have in a typical application. Custom data types like our own `Address` class, [Luxon’s DateTime](https://github.com/MikeMcl/decimal.js/), or [decimal.js’ Decimal](https://moment.github.io/luxon/).

# The JavaScript Type Issue
In JavaScript, there is currently no native way to find out what type properties are and instantiate them at runtime — the reason being that types don’t really exist in JavaScript. TypeScript types are syntactic sugar for development enjoyment.

The tools for runtime type inspection in JavaScript are:
1. `typeof` , which only works for primitive types
2. `instanceof` , which is only useful if you already know the class or constructor function you want to check against

# Give Way to reflect-metadata
Reflection is a common tool at the disposal of traditional OOP languages like Java and C#, and also languages like Go, Python, and Lua. In a nutshell, it’s a provided API that allows you to inspect a class or property at runtime, and get its type. This allows you to, among other things, create new instances from it.

The [reflect-metadata](https://github.com/rbuckton/reflect-metadata) proposal in JavaScript is not yet part of TC39, but it was authored by the person responsible for implementing Reflection in C#, so it’s safe to say that it will get there eventually.

Even though `reflect-metadata` is experimental, it has been heavily used by Angular 2 for many years now. Angular depends on it for its dependency injection mechanism, that is, creating and passing along resources when needed, at runtime.

It is like a key-value store that can only reside in classes or class properties. We can grab it and use it to auto-populate type metadata or manage our own custom metadata so that we can achieve all of our goals and dreams.

# Let’s Code a More Dynamic Approach
After installing:
`npm install reflect-metadata`'

And importing it at the beginning of our file:
`import 'reflect-metadata'`

We need to make sure our tsconfig.json contains the following:
```json
"compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
}
```

This will allow us to use decorators to trigger reflect-metadata to auto-populate the `design:type` metadata key in our decorated properties.

We will also need a few types:
```typescript
type Constructor<T = any> = { new(...args: any[]): T }

type Indexable = { [key: string]: any }

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends Array<infer U>
    ? Array<DeepPartial<U>>
    : DeepPartial<T[P]>
}
```
**Constructor**: Is used to represent constructor functions or classes, on which we can call new
**Indexable**: Is used to define indexable classes or objects on which you can do object[key]
**DeepPartial**: Is used to allow us to specify partial representations of objects or classes, as the provided Partial TypeScript utility only works for one level of depth

Now on to the decorators:
```typescript
function Primed(target: any, propertyKey: string) {}

function Model<T extends Constructor>(constructor: T){
  return class extends constructor {
    constructor(...args: any[]){
      super()
      this.init(args[0])
    }
  }
}
```

**Primed**: It doesn’t have a body but will be used to trigger `Reflect.metadata` to be added on the decorated property, as reflect-metadata only auto-populates the `design:type` metadata for properties that are decorated
**Model**: It will be used to override the decorated class’s constructor so that we can call our custom initialization method implicitly

We will create a `Base` class that will take care of initializing our properties, creating new instances when required:
```typescript
class Base<T> {
  constructor(payload: DeepPartial<T>){}

  private init(payload: any){
    for(const key in payload){
      if(this.hasOwnProperty(key)){
        const factory: Constructor = Reflect.getMetadata('design:type', this, key)
        (this as Indexable)[key] = factory ? new factory(payload[key]) : payload[key]
      }
    }
  }
}
```

You’ll notice we added a different method for initializing our instances, and our constructor is empty.

This is because we want to initialize A from within B so that we don’t have to copy the constructor to every class. If A extends B and A contains properties which have a default value, you can’t set A’s properties from within B’s constructor, as they will be overridden by A’s default values:
```typescript
class A{
  constructor(){
    this.foo = "bar"
  }
}

class A extends B {
  foo = null
}

console.log(new A())

// Output: A { foo: null }
```

And that’s why we have an `init` method. We’re making sure A gets fully initialized before setting properties in it.

So inside the `init` method, we call:
`Reflect.getMetadata('design:type', this, key)` to get the metadata that was auto-populated for that key in the instance, which will contain the value associated with the type assigned to the decorated property. If it exists, we create a new instance with new, passing the value into it.

Using our earlier example, the classes will now look like this:
```typescript
import { DateTime } from 'luxon'
import { Decimal } from 'decimal.js'

@Model
class Address extends Base<Address> {
  no: string = ''
  street: string = ''
  city: string = ''
}

@Model
class Person extends Base<Person> {
  name: string = ''
  lastName: string = ''
  
  @Primed
  dob: DateTime = DateTime.local()
  
  @Primed
  address: Address = new Address()
  
  @Primed
  netWorth: Decimal = new Decimal(0)
}
```

There’s a little problem with this. We’d get a type error if tried to do the following:
```typescript
const person = new Person({
  name: 'Alice',
  lastName: 'Liddell',
  dob: '1852-05-04T12:00:00.000Z',
  address: {
    street: 'East 74th Street',
    city: 'Manhattan'
  },
  netWorth: 99
})
```

That’s because we want to pass a `string` into our `dob` field and a `number` into our `netWorth` field, and our Person class is expecting a `DateTime` and a `Decimal` respectively. What we can do is modify our `Base` class to accept an optional second type, which we can use to create a new union type between it and the target class’s type.

This is what that would look like:
```typescript
type BaseConstructorPayload<T, U = undefined> = DeepPartial<U extends undefined ? T : T | U>

class Base<T, U = undefined> {
  constructor(payload: BaseConstructorPayload<T, U>){}

  ...
}
```
Which we can then use like so:
```typescript
interface PersonInput {
  dob: string
  netWorth: number
}

@Model
class Person extends Base<Person, PersonInput> {
  ...
}
```

# Dealing With Arrays and Other Custom Types
We’re almost there, but we still have a couple of problems:

1. `reflect-metadata` doesn’t populate `design:type` on arrays properly. It sets them to Array instead of the expected type.

2. Not all of our custom data types will be created/initialized the same. With Luxon’s `DateTime`, we’d want to initialize it with `DateTime.fromISO`. We’d want the flexibility with other types as well.

To address these, we need to be able to customize the way we specify what type something is when needed, and for that, we will introduce a new metadata key.

We’ll allow the `Primed` decorator to accept an optional parameter, which will be a class or function. We will then save that into the `CONSTRUCTOR_META` key with `Reflect.defineMetadata`:

```typescript
const CONSTRUCTOR_META = Symbol('CONSTRUCTOR_META')

export function Primed(constructor?: Constructor) {
  return (instance: any, propertyKey: string) => {
    if(constructor)
      Reflect.defineMetadata(CONSTRUCTOR_META, constructor, instance, propertyKey)  
  }
}
```

To deal with Luxon’s `DateTime` and other custom types that might be created different ways, we’ll check for them and initialize them manually through a new private function `parseValue`.

To deal with arrays, we’ll check the `design:type` metadata to know if we need to iterate.
We will be getting our new metadata under `CONSTRUCTOR_META`, which will take precedence over `design:type`:
```typescript
export class Base<T, U = undefined> {
  constructor(payload: BaseConstructorPayload<T, U>){}

  private init(payload: any){
    for(const key in payload){
      if(this.hasOwnProperty(key)){
        const designType: Constructor = Reflect.getMetadata("design:type", this, key)
        const constructorMeta: Constructor = Reflect.getMetadata(CONSTRUCTOR_META, this, key)
        const factory = constructorMeta || designType
        
        const isArray = designType === Array
        const value = isArray ? payload[key].map(v => this.parseValue(v, factory)) : this.parseValue(payload[key], factory)
        ;(this as Indexable)[key] = value
      }
    }
  }
  
  private parseValue(value: any, factory: Constructor){
    if(factory){
      if(factory === DateTime)
        return DateTime.fromISO(value)
      else if(factory === Decimal)
        return new Decimal(value)
      else if(factory.prototype instanceof Base.constructor)
        return new factory(value)
    }
    return value
  }
}
```

Finally, after making `addresses` an array, this will be our class definition and usage:
```typescript
interface PersonInput {
  dob: string
  netWorth: number
}

@Model
class Person extends Base<Person, PersonInput> {
  name: string = ''
  lastName: string = ''
  
  @Primed()
  dob: DateTime = DateTime.local()
  
  @Primed(Address)
  addresses: Address[] = []
  
  @Primed()
  netWorth: Decimal = new Decimal(0)
}


const person = new Person({
  name: 'Alice',
  lastName: 'Liddell',
  dob: '1852-05-04T12:00:00.000Z',
  address: [{
    street: 'East 74th Street',
    city: 'Manhattan'
  }],
  netWorth: 99
})
```

# Wrap Up
It’s safe to say that with factory functions you save yourself the initial setup at the cost of having to repeat yourself. You’d need to create both a factory function and an interface with its type definition.

You could do both, or (after overcoming a handful of obstacles) you could just do a class.

You can play around the code in this [codesandbox](https://codesandbox.io/s/primed-model-v8txy?file=/src/index.ts).

There are a couple of caveats when using `reflect-metadata` to auto-populate type metadata: It doesn’t handle self-references or circular references.
I actually made a simple package, [primed-model](https://www.npmjs.com/package/primed-model), that solves these problems, providing the outlined decorators and base class. It also provides a `clone` method, for preventing side effects when passing the class instance around. Check it out if you’re interested!

You can also check out [class-transformer](https://www.npmjs.com/package/class-transformer) if you want to see a different, on-steroids take on this task.

Let’s show classes some love!

That’s all, and thanks for reading.