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.