typescriptrecordcode-completionstrong-typing

Why does annotating this object with a Record type remove Intellisense?


I am creating a data-file that looks like this:

interface ISprite {
  textureName: string,
  frame: Frame,
  origin: Vec2,
  zIndex?: number
}

export let sprites: Record<string, ISprite> = {
  monster: {
    textureName: "monster",
    frame: new Frame(0, 0, 32, 41),
    origin: new Vec2(16, 28),
    zIndex: -1
  },
  player: {
    textureName: "player",
    frame: new Frame(0, 0, 32, 32),
    origin: new Vec2(15, 32)
  }
};

If I then try to import this data file from another file like so:

import { sprites } from "../data/sprites";

And then try to access a property like this:

let player = sprites.player;

Then I don't get Intellisense (code completion) when I type sprites.

I noticed, however, that if I remove the Record<string, ISprite> annotation from the sprites variable declaration that I do get intellisense.

However, I believe I require this annotation, because one of my functions only takes ISprite types, and I don't want to instead make it take an any.

Is it possible to maintain the strong typing while also having code completion?


Solution

  • It does that because Record<string, ISprite> can have any string as a key. The UI can't hint every string.

    You can give the UI further information to make it show monster and player for auto-completion by using an intersection between a type with those named properties and Record<string, ISprite> (but keep reading, you may not need to):

    export let sprites: {monster: ISprite, player: ISprite} & Record<string, ISprite> = {
    // −−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      monster: {
        textureName: "monster",
        frame: new Frame(0, 0, 32, 41),
        origin: new Vec2(16, 28),
        zIndex: -1
      },
      player: {
        textureName: "player",
        frame: new Frame(0, 0, 32, 32),
        origin: new Vec2(15, 32)
      }
    };
    

    Playground link

    However:

    I believe I require this annotation, because one of my functions only takes ISprite types.

    That's fine. Without any type annotation at all, the values your object has on its monster and player properties are compatible with ISprite, so you can pass them to a function expecting ISprite. TypeScript's type system is structural (based on shapes of objects), not nominal (based on names of types), so it's happy to accept any object as an ISprite as long as it has all the necessary properties with compatible types. This works just fine, for instance:

    export let sprites = {
        monster: {
          textureName: "monster",
          frame: new Frame(0, 0, 32, 41),
          origin: new Vec2(16, 28),
          zIndex: -1
        },
        player: {
            textureName: "player",
            frame: new Frame(0, 0, 32, 32),
            origin: new Vec2(15, 32)
        }
    };
    
    function example(sprite: ISprite) {
        console.log(sprite);
    }
    
    example(sprites.monster); // <=== Perfectly happy
    

    Playground link

    If you want the type name, or hinting for the members of ISprite as you're writing futher entries in sprites, you could give yourself a function:

    makeSprite(sprite: ISprite) {
        return sprite;
    }
    

    It looks like a do-nothing, and it is — at runtime. (Don't worry, it's not expensive). But at authoring time, it lets you do this:

    export let sprites = {
        monster: makeSprite({
          textureName: "monster",
          frame: new Frame(0, 0, 32, 41),
          origin: new Vec2(16, 28),
          zIndex: -1
        }),
        player: makeSprite({
            textureName: "player",
            frame: new Frame(0, 0, 32, 32),
            origin: new Vec2(15, 32)
        })
    };
    

    As you're adding entries, you'll be prompted with the property names from ISprite while you're writing the sprite argument for makeSprite.