2

I have an variable defined like this:

interface MyObjects {
  Troll : {
    strength : string;
    dexterity : string;
    wisdom : string;
  },
  Child : {
    health : string;
    wellness : string;
  },
  Kitten : {
    sneakFactor : string;
    color : string;
  }
}

let myObjects : MyObjects;

Is there a way to use TypeScript to populate myObjects so that each string contains the fully qualified name like this:

myObjects.Troll.strength = 'Troll_strength';
myObjects.Troll.dexterity= 'Troll_dexterity';
myObjects.Troll.wisdom = 'Troll_wisdom';
myObjects.Child.health = 'Child_health';
//etc

Update 1: I was hoping to use something like Object.keys( myObjects ) to get all the keys to then iterate over, but I can't get the keys of an uninitialized variable.


Update 2: I got a little further by using:

declare function keys<T extends object>() : Array<keyof T>;
const k = keys<MyObjects>();

I now have all the key names at the root saved in the k array. I can access each of the sub objects like this:

myObjects[k]

...but I'm now not sure how to get an array of all the sub properties of myObjects[k] since I don't have a type defined for each of those.

Marek Krzeminski
  • 1,308
  • 3
  • 15
  • 40
  • Are you looking for assigning those literal values programmatically, perhaps through reflection? – Ian MacDonald Jan 18 '19 at 19:38
  • I advise against reflection since transpilation and/or minification of the files can end up with name mangling which will most likely break most of those approaches. – Christian Ivicevic Jan 18 '19 at 19:42
  • I was hoping to do something like Object.keys( myObjects ), and then iterate over all the keys, but because myObjects is not populated I can't get the keys to iterate over. – Marek Krzeminski Jan 18 '19 at 19:47
  • The purpose of interface is to provide intellisense and and force types during development. They don't exist at runtime once they are converted to js. You can do this only if you create a create a `class` or implement this interface. – adiga Jan 18 '19 at 20:29
  • 1
    Possible duplicate of [Get keys of a Typescript interface as array of strings](https://stackoverflow.com/questions/43909566/get-keys-of-a-typescript-interface-as-array-of-strings) – adiga Jan 18 '19 at 20:30

1 Answers1

2

This is not possible with the standard TS compiler. TypeScript types are completely removed at runtime, so this information is no longer available.

However, if you compile your source with a custom script instead of just using tsc, this becomes possible.

In order to simplify the code, I've used ts-simple-ast here.

Is this a good idea? No... but it was fun.

import { Project, VariableDeclaration, Type, WriterFunction } from 'ts-simple-ast' // ^21.0.0

const project = new Project()
project.addExistingSourceFiles('src/**/*.ts')

// Custom functionality
// Look for variables that have a doc comment with `@autoInit`
// Get their type, and assign values to https://stackoverflow.com/q/54260406/7186598
for (const sourceFile of project.getSourceFiles()) {
    // TODO: Class properties, object literal properties, etc.?
    const declarations = sourceFile.getVariableDeclarations()

    for (const declaration of declarations.filter(hasAutoInitTag)) {
        if (declaration.hasInitializer()) {
            console.warn(`'${declaration.getName()}' has an initializer and @autoInit tag. Skipping.`)
            continue
        }

        const type = declaration.getType()
        const writer = createWriterForType(declaration.getName(), type);

        const parentStatement = declaration.getParent().getParent()
        const index = sourceFile.getStatements().findIndex(statement => statement === parentStatement)

        // Insert after the variable declaration
        sourceFile.insertStatements(index + 1, writer);
    }

    console.log(sourceFile.getFullText())
    // Uncomment once you have verified it does what you want.
    // sourceFile.saveSync()
}

// There's almost certainly a better way to do this.
function hasAutoInitTag(declaration: VariableDeclaration) {
    // Comments are attached to a VariableDeclarationList which contains VariableDeclarations, so
    // get the parent.
    const comments = declaration.getParent().getLeadingCommentRanges().map(range => range.getText())
    return comments.some(comment => comment.includes('@autoInit'))
}

function createWriterForType(name: string, type: Type): WriterFunction {
    return writer => {
        function writeTypeInitializer(nameStack: string[], type: Type) {
            if (type.isString()) {
                // Some logic for non-standard names is probably a good idea here.
                // this won't handle names like '()\'"'
                writer.writeLine(`${nameStack.join('.')} = '${nameStack.slice(1).join('_')}'`)
            } else if (type.isObject()) {
                writer.writeLine(`${nameStack.join('.')} = {}`)
                for (const prop of type.getProperties()) {
                    const node = prop.getValueDeclarationOrThrow()
                    writeTypeInitializer(nameStack.concat(prop.getName()), prop.getTypeAtLocation(node))
                }
            } else {
                console.warn('Unknown type', nameStack, type.getText())
            }
        }

        writeTypeInitializer([name], type)
    }
}

Now, the less exciting solution.

Instead of describing your object with an interface, you could generate the interface from an object. The keys would then be available to access with an autoInit function, which could generate the strings you wanted. Playground demo

// Exactly the same as the original interface
type MyObjects = typeof myObjects

let myObjects = {
    Troll: { 
        strength: '',
        dexterity: '',
        wisdom: ''
    },
    Child: {
        health: '',
        wellness: ''
    },
    Kitten: {
        sneakFactor: '',
        color: ''
    }
};

autoInit(myObjects)
console.log(myObjects)

type AutoInitable = { [k: string]: AutoInitable } | string
function autoInit(obj: { [k: string]: AutoInitable }, nameStack: string[] = []) {
    for (const key of Object.keys(obj)) {
        const val = obj[key]
        if (typeof val === 'string') {
            obj[key] = [...nameStack, key].join('_')
        } else {
            autoInit(val, nameStack.concat(key))
        }
    }
}

The question adiga linked is yet another option, which is probably better since it's more generic. You can access sub-types with MyObjects['Troll'], but I'm pretty sure you can't do this automatically to any depth as you can with the above two options.

Gerrit0
  • 7,955
  • 3
  • 25
  • 32