'Specifying the type for Threejs Object3D.userData in TypeScript
In Three.js there is a field on Object3D
class called userData, which in the type declaration file node_modules/three/src/core/Object3D.d.ts
is defined as
/**
* Base class for scene graph objects
*/
export class Object3D extends EventDispatcher {
[...]
/**
* An object that can be used to store custom data about the Object3d. It should not hold references to functions as these will not be cloned.
* @default {}
*/
userData: { [key: string]: any };
[...]
}
I wanted to more strongly type the userData, so I created a module type declaration src/typings/three.d.ts
:
declare module 'three' {
export class Object3D {
userData: MyType1 | MyType2;
}
}
type MyType1 = {
type: 'type1';
radius: number;
};
type MyType2 = {
type: 'type2';
name: string;
};
This, while it did overwrite the userData
property, it instead of merging, overwrote all type declarations in the three
module, making the change more damaging than useful (note the lack of other properties).
Is there a way to merge the type declarations in a way that only userData
is overwritten and not entire module?
Solution 1:[1]
In typescript jargon, what you're trying to do is "declaration merging" combined with "module augmentation" (see docs). There are a few problems with your sample so far, most of which is fixable, one of which is not (i.e. you've hit a limitation of the language).
I'm going to first explain these problems and (partial) solutions, and then suggest an alternative approach that will work completely.
- When you want to augment a class definition through module augmentation, your augmentation must be in the form of an un-exported interface, not an exported class definition. So instead of
export class Object3D
you must writeinterface Object3D
. (Not the most intuitive thing, I know - but the docs state "Not all merges are allowed in TypeScript. Currently, classes can not merge with other classes or with variables.") - You must match the signature of the class you are trying to augment exactly, including generic parameters and
extends
statements. So instead of...
...you'd need to write...interface Object3D {
(hat tip to this previous answer for pointing out this poorly-documented "gotcha")interface Object3D<E extends BaseEvent> extends EventDispatcher<E> {
- The particular way that
@types/three
is written, it's not just one big file with "export class Object3D..." type statements at the top-level. Instead, the root file of@types/three
has this statement:
In turn,export * from './src/Three';
src/Three
had (among other things) this statement:
And theexport * from './core/Object3D';
src/core/Object3D
finally has the core class definition:
I've found through experimentation (although I can't find a clear explanation as to why) that these intermediateexport class Object3D<E extends BaseEvent = Event> extends EventDispatcher<E> {
export *
statements get in the way of your module augmentation code actually matching up with the thing it's trying to affect (e.g. theObject3D
class definition in@types/three/src/core/Object3D
). To get around this, yourdeclare module
statement must target that file specifically, not the re-exported version of it.
So putting all this together, this will (almost) work:
import { BaseEvent, EventDispatcher } from "three";
declare module "three/src/core/Object3D" {
interface Object3D<E extends BaseEvent> extends EventDispatcher<E> {
// This works fine...
somethingElse: string;
// However, this does not (see below)
// Typescript will throw this error:
// Subsequent property declarations must have the same type. Property 'userData' must be of type '{ [key: string]: any; }'
userData: MyType1 | MyType2;
}
}
This final problem is a limitation of the language - declaration merging cannot change the type of an existing property on the original class definition (even if the new type is technically a sub-type of the original one). It can only add new properties to the class.
An Alternative Approach That Does Work
Instead of using declaration merging, you can make a new class declaration that extends the original, and override the type there:
declare module "three" {
import { BaseEvent } from "three/src/core/EventDispatcher";
import { Object3D as Object3DOriginal } from "three/src/core/Object3D";
export * from "three/src/Three";
export class Object3D<E extends BaseEvent = Event> extends Object3DOriginal<
E
> {
userData: MyType1 | MyType2;
}
}
Check out this codesandbox for a working example.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Andrew Stegmaier |