TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

unable to augment class definition

Open trusktr opened this issue 6 years ago • 11 comments

Search Terms

typescript unable to augment class

Suggestion

I keep running into cases where I want to augment a class that I import from another module.

I know it isn't best practice, but in some cases it is necessary.

Use Cases

F.e. extending a prototype

Examples

At the moment, I'd like to do something like

import {MyClass} from './MyClass';

declare module './MyClass' {
  interface MyClass {
    newMethod(s: string): void;
  }
}

MyClass.prototype.newMethod = (s: string) => { /* ... */};
  // ^---- ERROR, 'MyClass' only refers to a type, but is being used as a value here. ts(2693)

But it won't let me. When I try to do this, tsc complains

'MyClass' only refers to a type, but is being used as a value here. ts(2693)

I always run into this issue when attempting to augment a class.

Oddly, this is the method prescribed over at Module Augmentation, but every time I try it it simply does not work.

Also the other most visible StackOverlow answer by @basarat says it isn't supported with classes.

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

trusktr avatar Jun 30 '19 18:06 trusktr

In that example you apparently haven't imported the class constructor. So what is it actually importing? This is where I'd ask for a minimum reproducible example but suddenly I realize this is not Stack Overflow. Your code works for me so this may be an issue you're having in your environment as opposed to a deficiency in TypeScript itself?

jcalz avatar Jun 30 '19 19:06 jcalz

The actual class I'm importing is Object3D from the three module, which is a class like

// https://unpkg.com/[email protected]/src/core/Object3D.d.ts

export class Object3D extends EventDispatcher {
  // ...
}

The following works:

import {Object3D} from 'three'

Object3D.prototype.updateMatrix = function() {
  // override the implementation. No type errors yet.
}

But when I try to augment the Object3D class like the following,

import {Object3D, Vector3} from 'three'

declare module 'three' {
    interface Object3D {
        pivot: Vector3
    }
}

Object3D.prototype.updateMatrix = function() {
  // ^---- ERROR here

  // override the implementation (uses this.pivot)
}

I get

'Object3D' only refers to a type, but is being used as a value here. ts(2693)

Seems like I'm importing a class. I do know it happens all too often, so I end up not doing any augmentation like I want in a bunch of cases.

I've no clue what causes it not to work. Maybe TS needs a better error here?

trusktr avatar Jul 01 '19 03:07 trusktr

Classes/constructor functions have two interfaces, they have the "static" interface and they have instance interface. There are two ways to fix this problem:

import {Object3D, Vector3} from 'three'

declare module 'three' {
    interface Object3D {
        pivot: Vector3
    }

    interface Object3DConstructor {
        new (): Object3D;
    }

    const Object3D: Object3DConstructor;
}

Object3D.prototype.updateMatrix = function() {
  // override the implementation (uses this.pivot)
}

or

import {Object3D, Vector3} from 'three'

declare module 'three' {
    class Object3D {
        pivot: Vector3
    }
}

Object3D.prototype.updateMatrix = function() {
  // override the implementation (uses this.pivot)
}

In the first, you keep the two interfaces seperate, and this is the way a lot of the built in classes are modelled in typings and better models constructor functions, the latter uses the class keyword, which fuses the two interfaces together, just like they are at runtime.

I don't think TypeScript can really provide a clearer message, because it can't read your mind of what your intent it, it can only state the facts.

kitsonk avatar Jul 01 '19 06:07 kitsonk

I think we would benefit from having a link to a web IDE with the relevant dependency already configured, like maybe this (note, you might have to "Run" it to see the errors... or wait, or something. It seems to take a while for the red squigglies to show up in the editor).

Here, I have the following code in index.ts, which errors as @trusktr is reporting:

import { Object3D } from 'three';

declare module 'three' {
  // comment the following out to see error go away
  interface Object3D { }
}

const object3D: Object3D = new Object3D(); // error!
object3D.updateMatrix(); // error!

But I also have the following code, which works just fine:

import { Object7D } from './seven';

declare module './seven' {
  interface Object7D { }
}

const object7D: Object7D = new Object7D(); // okay
object7D.updateMatrix(); // okay

Now seven.d.ts is just a made-up declaration file I've placed inside my own project, with the following contents:

export class Object7D {
  constructor();
  updateMatrix(): void;
}

But the configured three dependency is similar, with code like

export class Object3D extends EventDispatcher {
	constructor();
// ... snip ...	
	updateMatrix(): void;
// ... snip ...
}

So, what's going on? What's the difference between seven and three here? (don't say four pls) I don't understand why in the case of seven, the compiler remembers that the static side of Object7D still exists as a value named Object7D, while in the case of three the compiler immediately forgets that Object3D was the name of a value as well as the name of a type.

I don't know enough about TS dependency management to know if this is a bug, design limitation, or user error, but it seems fishy to me. Can anyone speak authoritatively about this?

jcalz avatar Jul 01 '19 13:07 jcalz

What's the difference between seven and three here?

four. :trollface:

fatcerberus avatar Jul 01 '19 16:07 fatcerberus

@kitsonk Your first suggestion,

declare module 'three' {
    interface Object3D {
        pivot: Vector3
    }

    interface Object3DConstructor {
        new (): Object3D;
    }

    const Object3D: Object3DConstructor;
}

results in all other properties not existing on instances of Object3D. So things like obj.rotation result in Property 'rotation' does not exist on type 'Object3D'.

Your second example,

declare module 'three' {
    class Object3D {
        pivot: Vector3
    }
}

results in the same errors about all other properties missing, but also with additional errors inside the Object3D.prototype.updateMatrix override:

Screen Shot 2019-07-13 at 7 52 22 PM

It seems that with the first of your approaches, everything is any inside the updateMatrix override. Look in the middle of the following shot which uses the first approach, where I'm hovering on this and it shows as any:

Screen Shot 2019-07-13 at 7 50 52 PM

trusktr avatar Jul 14 '19 02:07 trusktr

I'm not able to reproduce the problem.

// ./world.d.ts

export class Foo {
    bar(): void;
}
// ./hello.ts

import {Foo} from "./world";

declare module "./world" {
    interface Foo {
        blah(): void
    }
}

Foo.prototype.blah = function() {

}

// all work fine
let x = new Foo();
x.bar();
x.blah();

DanielRosenwasser avatar Jul 14 '19 04:07 DanielRosenwasser

@DanielRosenwasser Seems to be a problem with the three/Object3D definition. Here's a reproduction: https://repl.it/@trusktr/QuarterlyWellinformedArchitect (press the "run" button).

trusktr avatar Jul 14 '19 05:07 trusktr

I updated the previous comment's broken link (in case you were reading email).

trusktr avatar Jul 14 '19 05:07 trusktr

declare namespace React {
  namespace Component {
    const whyDidYouRender: WhyDidYouRender.WhyDidYouRenderComponentMember;
  }
}

worked for me somehow. see: https://github.com/welldone-software/why-did-you-render/blob/6a85ed215279840d0eedbea5d86eba92cfb1291b/types.d.ts

vzaidman avatar Jan 25 '20 12:01 vzaidman

@trusktr Did you resolve this issue. Currently experiencing the same thing while augmenting classes imported from three The module augmentation seems to simply overwrite the declaration. I cant add properties that I need. Also getting hit with the "X only refers to a type, but is being used as a value here" when using new

betaupsx86 avatar May 20 '24 05:05 betaupsx86

Please open a new issue with a concrete repo if you're encountering problems.

RyanCavanaugh avatar Aug 06 '24 18:08 RyanCavanaugh