1

Typescript newbie here. Let's say I have a type coming from a library that looks like this:

type FooType {
  name: string;
  // Has much more attributes in real life
}

I now want to define a class called Foo like this:

import { FooType } from 'my-library';

class Foo {
  constructor(data: FooType) {
    Object.assign(this, data);
  }
}

With this code, I'm able to define a foo instance, yet I have an issue with autocomplete:

const foo = new Foo({ name: 'foo name' });
// Typing in foo.name does not bring any type information about the "name" attribute

Is there a way I can make a class automatically "inherit" all attributes from a type without having to type them manually?

Edit after being marked as duplicate:

What I want to achieve is to avoid manually typing attributes that are already existing on a type.

Thanks to @Phil I've been provided an answer that mentions this as an ongoing issue within Typescript: https://github.com/microsoft/TypeScript/issues/26792

What I will do for now is the following:

class Foo {
  constructor(public _data: FooType)
    Object.assign(this, _data);
  }

  get() {
    return this._data;
  }
}

const foo = new Foo({ name: 'Bar' });
foo.get().name; // After typing "foo.get()" the autocomplete works properly.
mdmb
  • 4,833
  • 7
  • 42
  • 90
  • 2
    You mean `class Foo implements FooType { … }`? – Bergi Jan 30 '23 at 02:22
  • 1
    Follow this issue ~ [Javascript: Object.assign to assign property values for classes is not respected #26792](https://github.com/microsoft/TypeScript/issues/26792) – Phil Jan 30 '23 at 02:26
  • Try this [code](https://www.typescriptlang.org/play?module=1&ssl=7&ssc=11&pln=7&pc=21#code/PTAEEsFsAcHsCcAuoDeoBitYBUCe0BTUAX1ADN5ZJQBySXAWgBtwAjeAQ3lxoG4AofuAB2iAvDIcAxkUw58RFP1ChhHSAQBcoAM6J4IgOb9igqUw46dGLBBhMCG0dbl5CqQSugBXViymq6gQAhNp6BsLGyqBSsMLh3lKICAAUACYciBzargoAlB4qKgDyrABWBEkAdJY64IbCKYgAFuA6ADSgGVl50aam-LHxyGS2ALyqBADuNrApaGoa2jSjsIEaNCR5AoNxOrAOVUywhimrVYsEvUA) – Asleepace Jan 30 '23 at 02:28
  • 1
    @Asleepace I want to avoid defining these e.g. "public name: string" for all properties defined in the FooType. The only way I was managed to avoid it was with `constructor(public data: FooType)` yet then the properties are available under e.g. `foo.data.name` instead of `foo.name` – mdmb Jan 30 '23 at 02:33
  • @Phil This is helfpul. As far as I understand – this is an issue with Typescript, with a few, messy, workarounds – mdmb Jan 30 '23 at 02:34
  • @mdmb you could use the `Partial` keyword – Asleepace Jan 30 '23 at 02:44
  • 2
    @mdmb or you could cast `as FooType` which seems to work as well [code](https://www.typescriptlang.org/play?module=1#code/PTAEEsFsAcHsCcAuoDeoBitYBUCe0BTUAX1ADN5ZJQBySXAWgBtwAjeAQ3lxoG4AofuAB2iAvDIcAxkUw58RFP1ChhHSAQBcoAM6J4IgOb9igqUw46dGLKmWgpsYXvgBXKYgQAKACYdEHNpyeIQAlHYqKgDyrABWBB4AdJY64IbCXogAFuA6ADSgfgGh9qam-I7OyGS2ALyqBADuNrBeaGoa2jQ1sKrqBDQk4ZYtIQRmTjqwTASJTLCGXj2JHQQlQA) – Asleepace Jan 30 '23 at 02:51
  • @Asleepace That's a good idea – tried that, yet it starts to complain as soon as you add new stuff to the `Foo` class. The workaround that is to cast it as e.g. `[...] as Foo & Partial` yet I think about it as a bad developer experience, as you have to remember about casting every time you create a new `foo`. – mdmb Jan 30 '23 at 11:06

1 Answers1

-1

Note: this is just a workaround and doesn't fix the underlying issue with using Object.assign() in the constructor with TypeScript.

https://github.com/Microsoft/TypeScript/issues/16672

Update: this question definitely lead me down quite the rabbit and I wasn't quite happy with my previous response so here is something I think would work better for solving these two items:

  • Creating a class which extends an object
  • Allowing TypeScript to still do its thing

Step One: use the typescript mixin pattern to return an anonymous class with the properties of a generic object T:

function GenericBase<T>(data: T) {
  return class {
    constructor() {
      Object.assign(this, data)
    }
  } as ({
      new (...args: any[]): T;
  })
}

Now we have an anonymous function with a constructor which is then casted as a class that returns our generic object T.

Step Two: Note that we don't need to touch the above code we just need to extend it, so for OP's example we would do this:

class MyClass extends GenericBase(foo) {

  constructor() {
    super()
  }

  greet() {
     return `Hello, ${this.name}!`
  }
}

More examples:

const myClass = new MyClass()
console.log(myClass.name)       // bar
console.log(myClass.greet())    // Hello, bar!


function doSomethingWithFoo(someFoo: Foo) {
  // example method that only takes types Foo
}

doSomethingWithFoo(myClass)

Try it on the TypeScript playground!

Asleepace
  • 3,466
  • 2
  • 23
  • 36
  • This doesn't solve the problem to "*make a class automatically "inherit" all attributes from a type without having to type them manually*", does it? A `GenericSubClass` or `ProxySubclass` instance is still not compatible with `FooType`, is it? – Bergi Jan 30 '23 at 23:32
  • The proxy appears superfluous, incomplete and inefficient, slowing down all code that uses the object created by `.new()`. Wouldn't `return Object.assign(newClassInstance, data) as ProxySubClass & FooType;` achieve basically the same? – Bergi Jan 30 '23 at 23:34
  • @Bergi the difference here is that the proxy will lookup the property on the sub-class first before the parent class, which allows us to override properties and methods vs. just using `Object.assign`. – Asleepace Jan 30 '23 at 23:41
  • If that is what you want to achieve, you could also just loop over the data and copy only those properties that do not already exist on the target. – Bergi Jan 30 '23 at 23:47
  • @Bergi this way we also retain the base-class values which can be used if we decide to override them, such as adding `Sir ${name}` to the name property. The `getter`'s & `setter`'s first do a lookup on the sub-class before checking the base class which is more similar to the prototype chain vs. merging objects conditionally. – Asleepace Jan 31 '23 at 00:12
  • All of this could be achieved statically as well, without a proxy. But I really doubt that the OP events wants this. – Bergi Jan 31 '23 at 00:15
  • @Bergi I'm sure there are several different ways to achieve something like this, but I don't think there is anything wrong with using a proxy sparingly. – Asleepace Jan 31 '23 at 00:18