在TypeScript中为Threejs Object3D.userData指定类型

7
在 Three.js 中,Object3D 类有一个名为 userData 的字段,它在类型声明文件 node_modules/three/src/core/Object3D.d.ts 中定义为:
/**
 * 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 };

[...]

}

我希望更加强化userData的类型检查,所以我创建了一个模块类型声明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;
};

尽管这样做确实覆盖了userData属性,但它并没有进行合并,而是覆盖了three模块中的所有类型声明,这使得更改比有用更具破坏性(请注意其他属性的缺失)。 在vscode中缺少类型声明 是否有一种方法可以将类型声明合并,从而仅覆盖userData而不是整个模块?
1个回答

4
在Typescript的术语中,您正在尝试进行“声明合并”以及“模块增强”(请参见 文档)。到目前为止,您的示例存在一些问题,其中大部分可修复,其中一个无法解决(即,您已经达到了该语言的限制)。
我将首先解释这些问题和(部分)解决方案,然后建议一个完全可行的替代方法。
  1. 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 write interface 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.")
  2. You must match the signature of the class you are trying to augment exactly, including generic parameters and extends statements. So instead of...
    interface Object3D {
    
    ...you'd need to write...
    interface Object3D<E extends BaseEvent> extends EventDispatcher<E> {
    
    (hat tip to this previous answer for pointing out this poorly-documented "gotcha")
  3. 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:
    export * from './src/Three';
    
    In turn, src/Three had (among other things) this statement:
    export * from './core/Object3D';
    
    And the src/core/Object3D finally has the core class definition:
    export class Object3D<E extends BaseEvent = Event> extends EventDispatcher<E> {
    
    I've found through experimentation (although I can't find a clear explanation as to why) that these intermediate export * statements get in the way of your module augmentation code actually matching up with the thing it's trying to affect (e.g. the Object3D class definition in @types/three/src/core/Object3D). To get around this, your declare module statement must target that file specifically, not the re-exported version of it.

将所有这些内容综合起来,这个方案(几乎)可以实现:

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;
  }
}

这个最终的问题是语言本身的限制——声明合并无法更改原始类定义中现有属性的类型(即使新类型在技术上是原始类型的子类型)。它只能向类中添加新属性。
一个可行的替代方法是通过创建一个扩展了原始类的新类声明,然后在这里覆盖类型。不要使用声明合并。
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;
  }
}

请查看此 codesandbox 示例,以获取一个可工作的示例。


这个解决方案在简单情况下有效,但在大多数情况下无效。例如,getObjectByName仍然返回原始的Object3D类型,而不是被覆盖的类型。请参见https://codesandbox.io/s/stackoverflow-module-augmentation-forked-47yev7 - Mitchell Loeppky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接