minecraft-addon-tools / minecraft-scripting-types

TypeScript typings for the Minecraft Scripting API
Other
44 stars 3 forks source link

Allow strongly typed custom overload of createComponent/getComponent using interface map #2

Open Dykam opened 5 years ago

Dykam commented 5 years ago

Similar to how dom.d.ts uses an interface to map stringly typed names to actual elements: (warning, large file) https://github.com/Microsoft/TypeScript/blob/b830ef8/src/lib/dom.generated.d.ts#L4247

This can then be augmented using a global declaration:

declare global {
  interface ComponentTypeMap {
    "my:custom": MyComponent.Custom;
  }
}
AtomicBlom commented 5 years ago

I'm confused, is that not exactly what we're doing here? https://github.com/minecraft-addon-tools/minecraft-scripting-types/blob/master/packages/server/types/server_system_components.generated.d.ts

Dykam commented 5 years ago

Sorry, I wasn't clear. Right now the result would be the same, but for someone to add two strongly typed overloads one needs to add both using a declare global, my suggestion pretty much ammounts to reducing that to a single place, using an interface to map the string names to types. Which then can also be consumed in different places to produce similarly overloaded functions.

abc-55 commented 5 years ago

I was thinking of doing this, but for different reasons - to allow making custom functions that accept components.

declare interface ComponentTypeMap {
  "minecraft:position": IPositionComponent;
  // ...
}

getComponent<K extends keyof ComponentTypeMap>(entity: IEntityObject, componentIdentifier: K): ComponentTypeMap[K] | null;
Dykam commented 5 years ago

Possibly more powerful even is that it allows something like this:

function foo<T extends ComponentTypeMap[keyof ComponentTypeMap]>(component: T): T {
    return component;
}
AtomicBlom commented 5 years ago

Huh, this looks like an area of TypeScript I haven't played with before, but I'm keen to try it out, this sounds like a nice improvement.

abc-55 commented 5 years ago

The downside of this is that we need to create an interface that is not intended to be implemented by anything.

Dykam commented 5 years ago

Those interfaces even exist in the default libraries of Typescript. People don't randomly implement interface, so that's fine.

And maybe one could actually write a function returning Partial<ComponentTypeMap> with e.g. all components of an entity, so it might even be implemented.

AtomicBlom commented 5 years ago

I'm having mixed success with attempting this. At first glance everything seems to be fine when you open a file, then as soon as I made a change, TypeScript's language server would get very confused.

const system = server.registerSystem(0, 0);

const enum MyComponent {
    Test = "test:test"
}

interface ITestComponent {
    x: number;
}

declare interface ComponentTypeMap {
    [MyComponent.Test]: ITestComponent
}

system.initialize = function() {
    system.listenForEvent(ReceiveFromMinecraftServer.EntityCreated, onEntityCreated);
}

function onEntityCreated(entity: IEntityObject) {
    const component: IAttackComponent = system.getComponent(entity, MinecraftComponent.Attack)
    component.damage = [0, 1];

    if (system.hasComponent(entity, MinecraftComponent.CollisionBox)) {
        const test: ICollisionBoxComponent = system.getComponent(entity, MinecraftComponent.CollisionBox);
    }

    if (system.hasComponent(entity, MyComponent.Test)) {
        const test2: ITestComponent = system.getComponent(entity, MyComponent.Test);
    }
}

image

All of the MinecraftComponents are specified in the ComponentTypeMap, and IntelliSense is correctly merging the two declarations of ComponentTypeMap together, however when attempting to use the custom component it's refusing it see it as a valid parameter.

Further investigation required

Dykam commented 5 years ago

I don't have the code you have I think, so I can't test myself, but I think you'll probably need to put the declaration in a separate file, and declare as follows:

declare "minecraft-scripting-types-server" {
    declare interface ComponentTypeMap {
        [MyComponent.Test]: ITestComponent
    }
}

Otherwise you might just be declaring a new ComponentTypeMap, and typescript gets confused.

Dykam commented 5 years ago

Nevermind, just wrap your current declare in an declare global {}, and use a string key directly. That works for me. After that you can use the enum for accessing, but I'm not sure why it doesn't work in the definition.