typescript

Can I use template string types to automatically infer types for globalThis keys in TypeScript?


In my Three.js debugging workflow, I often attach objects to globalThis for convenience. However, manually declaring types for each property is becoming tedious as the globalThis object grows larger.

I want to know if TypeScript can automatically infer types for keys that end with a specific string, such as Material.

For example, I would like all properties on globalThis that end with Material (i.e., keys matching the template string type ${string}Material) to be treated as THREE.MeshPhysicalMaterial.

declare global {
    var camera: THREE.PerspectiveCamera
    var scene: THREE.Scene
    var renderer: THREE.WebGLRenderer
    var setCameraToLeft: () => void
    var setCameraToRight: () => void
    var bigBoxMaterial: THREE.MeshPhysicalMaterial
    var middleBoxMaterial: THREE.MeshPhysicalMaterial
    var smallBoxMaterial: THREE.MeshPhysicalMaterial
    ...Material: THREE.MeshPhysicalMaterial
}

Perhaps it is a definition similar to this...

declare global {
    var camera: THREE.PerspectiveCamera
    var scene: THREE.Scene
    var renderer: THREE.WebGLRenderer
    var setCameraToLeft: () => void
    var setCameraToRight: () => void
    [key: string]: key extends `${string}Material` ? THREE.MeshPhysicalMaterial : unknown
}

Solution

  • Looks like you want to merge an index signature into typeof globalThis. Unfortunately that's not possible; there's a feature request at microsoft/TypeScript#57353 for that, but it doesn't have much engagement, so I doubt we'll see this soon.

    The closest I can imagine getting is to merge an index signature into an interface that corresponds to the global object, such as the Window interface in the DOM:

    declare global {
        interface Window {
            [k: `${string}Material`]: THREE.MeshPhysicalMaterial
        }
    }
    

    That's using a template string pattern index signature to say that every key ending in Material will have a property of type THREE.MeshPhysicalMaterial (which isn't likely to be true, right? Maybe you want | undefined in there or maybe you want to use --noUncheckedIndexedAccess)

    Then you can index into window (but not into globalThis or use a global variable) to access things:

    window.randomMaterial.alphaHash; // okay
    

    You could also possibly redeclare globalThis like

    declare let globalThis: Window
    

    and then globalThis.randomMaterial.alphaHash works, but this is getting even more fragile.

    So you can get somewhat closer to your goal than just giving up, but I don't know that it's worth the effort. In some sense it's no better than just using a type assertion on a variable that references globalThis, and not trying to actually mess with merging at all:

    const myGlobal = globalThis as typeof globalThis & {
        [k: `${string}Material`]: THREE.MeshPhysicalMaterial
    };
    myGlobal.randomMaterial.alphaHash; // okay
    

    Playground link to code