@itrocks/property-type
Version:
Runtime type reflection from TypeScript declaration files for properties
200 lines • 7.07 kB
JavaScript
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
import { readFileSync } from 'node:fs';
import { dirname, normalize } from 'node:path';
import ts from 'typescript';
export class PropertyType {
type;
optional;
constructor(type, optional = false) {
this.type = type;
this.optional = optional;
}
get lead() { return this.type; }
}
export class CanonicalType extends PropertyType {
constructor(type) { super(type); }
}
export class CollectionType extends PropertyType {
elementType;
constructor(type, elementType) {
super(type);
this.elementType = elementType;
}
}
export class CompositeType extends PropertyType {
types;
constructor(types) {
super(types[0].type);
this.types = types;
}
get lead() { return this.types[0].lead; }
}
export class RecordType extends PropertyType {
keyType;
elementType;
constructor(keyType, elementType) {
super(Object);
this.keyType = keyType;
this.elementType = elementType;
}
}
export class IntersectionType extends CompositeType {
}
export class LiteralType extends PropertyType {
value;
constructor(value) {
super(literalValueType(value));
this.value = value;
}
}
export class TypeType extends PropertyType {
args;
constructor(type, args) {
super(type);
this.args = args;
}
}
export class UnionType extends CompositeType {
}
export class UnknownType extends PropertyType {
raw;
constructor(raw) {
super(undefined);
this.raw = raw;
}
}
export function isCanonical(propertyType, type) {
return (propertyType instanceof CanonicalType) && ((arguments.length === 1) || (propertyType.type === type));
}
export function isLiteral(propertyType, literal) {
return (propertyType instanceof LiteralType) && ((arguments.length === 1) || (propertyType.value === literal));
}
export function isType(propertyType, type) {
return (propertyType instanceof TypeType) && ((arguments.length === 1) || (propertyType.type === type));
}
function literalValueType(literal) {
switch (typeof literal) {
case 'bigint': return BigInt;
case 'boolean': return Boolean;
case 'number': return Number;
case 'string': return String;
case 'symbol': return Symbol;
}
}
function nodeToCanonicalType(node) {
const kind = node.kind;
const kinds = ts.SyntaxKind;
switch (kind) {
case kinds.BigIntKeyword: return new CanonicalType(BigInt);
case kinds.BooleanKeyword: return new CanonicalType(Boolean);
case kinds.NumberKeyword: return new CanonicalType(Number);
case kinds.ObjectKeyword: return new CanonicalType(Object);
case kinds.StringKeyword: return new CanonicalType(String);
case kinds.SymbolKeyword: return new CanonicalType(Symbol);
}
}
function nodeToLiteralType(node) {
if (!ts.isLiteralTypeNode(node))
return;
const kinds = ts.SyntaxKind;
const literal = node.literal;
switch (literal.kind) {
case kinds.FalseKeyword: return new LiteralType(false);
case kinds.NullKeyword: return new LiteralType(null);
case kinds.TrueKeyword: return new LiteralType(true);
case kinds.UndefinedKeyword: return new LiteralType(undefined);
}
if (ts.isNumericLiteral(literal)) {
return new LiteralType(+literal.text);
}
if (ts.isStringLiteral(literal)) {
return new LiteralType(literal.text);
}
}
function nodeToType(node, typeImports) {
if (ts.isArrayTypeNode(node)) {
return new CollectionType(Array, nodeToType(node.elementType, typeImports));
}
if (ts.isIntersectionTypeNode(node)) {
return new IntersectionType(node.types.map(node => nodeToType(node, typeImports)));
}
if (ts.isUnionTypeNode(node)) {
return new UnionType(node.types.map(node => nodeToType(node, typeImports)));
}
return nodeToCanonicalType(node)
?? nodeToLiteralType(node)
?? nodeToTypeType(node, typeImports)
?? new UnknownType(node.getText());
}
function nodeToTypeType(node, typeImports) {
if (!ts.isTypeReferenceNode(node))
return;
const name = node.typeName.getText();
const args = node.typeArguments?.map(node => nodeToType(node, typeImports));
return ((name === 'Record') && (args?.length === 2))
? new RecordType(args[0], args[1])
: new TypeType(strToType(name, typeImports), args);
}
export function propertyTypesFromFile(file) {
const content = readFile(file);
const filePath = dirname(file);
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
const propertyTypes = {};
const typeImports = {};
function parseNode(node) {
if (ts.isImportDeclaration(node) && node.importClause) {
let importPath = node.moduleSpecifier.text;
if ((importPath[0] === '.') && !importPath.endsWith('.js')) {
importPath += '.js';
}
const importFile = (importPath[0] === '.')
? normalize(filePath + '/' + importPath)
: importPath;
if (node.importClause.name) {
typeImports[node.importClause.name.getText()] = { import: importFile, name: 'default' };
}
const namedBindings = node.importClause.namedBindings;
if (namedBindings && ts.isNamedImports(namedBindings)) {
for (const importSpecifier of namedBindings.elements) {
const name = importSpecifier.name.getText();
const alias = importSpecifier.propertyName?.getText() ?? name;
typeImports[alias] = { import: importFile, name };
}
}
}
if (ts.isClassDeclaration(node)
&& node.name
&& node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
const className = node.name.getText();
typeImports[className] = { import: file, name: className };
for (const member of node.members) {
if (ts.isPropertyDeclaration(member) && member.type) {
const type = nodeToType(member.type, typeImports);
type.optional = !!member.questionToken;
propertyTypes[member.name.text] = type;
}
}
return;
}
ts.forEachChild(node, parseNode);
}
parseNode(sourceFile);
return propertyTypes;
}
function readFile(file) {
try {
return readFileSync(file.substring(0, file.lastIndexOf('.')) + '.d.ts', 'utf8');
}
catch (exception) {
console.error('file', file);
throw exception;
}
}
function strToType(type, typeImports) {
const typeImport = typeImports[type];
return typeImport
? require(typeImport.import)[typeImport.name]
: globalThis[type];
}
//# sourceMappingURL=property-type.js.map