Classes & Keyof in Typescript

Viewed 8568

Beginning with TypeScript, I am trying to use keyof to define dynamic properties on a class:

type UserType = {
  id: number,
  name: string,
}

class Domain<T> {
  _data: T;
  [K in keyof T]: T[K]; // Does not build:
                        // (1) A computed property name must be of type 
                        //     'string', 'number', 'symbol', or 'any'
                        // (2) Cannot find name 'keyof'


  constructor(data: T) {
    this._data = data;
    Object.keys(data).forEach((key) => Object.defineProperty(this, key, { get: () => this._data[key] }));
  }
}

const joeData: UserType = {
  id: 1,
  name: 'Joe',
}

const joe = new Domain(joeData); // type: Domain<UserType>
console.log(joe.id);

I did write this code using the same (working) syntax as:

type Foo<T> = {
  [K in keyof T]: T[K];
}

Any hints on how to fix the issue?

2 Answers

Mapped types can't be used in interfaces or classes. They can only be used within a type alias.

Interface and class inheritance don't mix well with mapped types if there are generics involved. Typescript wants to know the shape of the base class/interface upfront, so while we can write something like :

interface Foo extends Pick<UserType, keyof UserType> {

}

We can't write

interface Foo<T> extends Pick<T, keyof T> { //An interface can only extend an object type or intersection of object types with statically known members

}

This removes any possibility of fooling the compiler with a class-interface merge.

The only workaround is to define the class separately, and use a custom constructor signature. The custom constructor signature can have a generic type parameter and can use it in the instance type returned. We can make the returned type an instance of the Domain class in an intersection with T:

type UserType = {
    id: number,
    name: string,
}

class _Domain<T>  {
    _data: T;
    constructor(data: T) {
        this._data = data;
        Object.keys(data).forEach((key) => Object.defineProperty(this, key, { get: () => this._data[key] }));
    }
}

const Domain = _Domain as ({
    new <T>(data: T): _Domain<T> & T
})

const joeData: UserType = {
    id: 1,
    name: 'Joe',
}

const joe = new Domain(joeData); // type: _Domain<UserType> & UserType
console.log(joe.id);

I think that there is no way to dynamically specify class properties like this. It does not work in an interface either. I tried a variation of your class with implements Foo<T> and got this error:

A class can only implement an object type or intersection of object types with statically known members.

That leads me to believe that Typescript is specifically designed to avoid dynamically-specified class properties.

You can do this with a constructor that is not a class. You can use your Foo type as the return type of a function that constructs a plain object with getters. Or if all you need is a read-only version of an object you can use Object.freeze(data).

Related