json-schema-to-typescript
Version:
compile json schema to typescript typings
314 lines (312 loc) • 13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateType = exports.generate = void 0;
const lodash_1 = require("lodash");
const index_1 = require("./index");
const AST_1 = require("./types/AST");
const utils_1 = require("./utils");
function generate(ast, options = index_1.DEFAULT_OPTIONS) {
return ([
options.bannerComment,
declareNamedTypes(ast, options, ast.standaloneName),
declareNamedInterfaces(ast, options, ast.standaloneName),
declareEnums(ast, options),
]
.filter(Boolean)
.join('\n\n') + '\n'); // trailing newline
}
exports.generate = generate;
function declareEnums(ast, options, processed = new Set()) {
if (processed.has(ast)) {
return '';
}
processed.add(ast);
let type = '';
switch (ast.type) {
case 'ENUM':
return generateStandaloneEnum(ast, options) + '\n';
case 'ARRAY':
return declareEnums(ast.params, options, processed);
case 'UNION':
case 'INTERSECTION':
return ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '');
case 'TUPLE':
type = ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '');
if (ast.spreadParam) {
type += declareEnums(ast.spreadParam, options, processed);
}
return type;
case 'INTERFACE':
return getSuperTypesAndParams(ast).reduce((prev, ast) => prev + declareEnums(ast, options, processed), '');
default:
return '';
}
}
function declareNamedInterfaces(ast, options, rootASTName, processed = new Set()) {
if (processed.has(ast)) {
return '';
}
processed.add(ast);
let type = '';
switch (ast.type) {
case 'ARRAY':
type = declareNamedInterfaces(ast.params, options, rootASTName, processed);
break;
case 'INTERFACE':
type = [
(0, AST_1.hasStandaloneName)(ast) &&
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
generateStandaloneInterface(ast, options),
getSuperTypesAndParams(ast)
.map(ast => declareNamedInterfaces(ast, options, rootASTName, processed))
.filter(Boolean)
.join('\n'),
]
.filter(Boolean)
.join('\n');
break;
case 'INTERSECTION':
case 'TUPLE':
case 'UNION':
type = ast.params
.map(_ => declareNamedInterfaces(_, options, rootASTName, processed))
.filter(Boolean)
.join('\n');
if (ast.type === 'TUPLE' && ast.spreadParam) {
type += declareNamedInterfaces(ast.spreadParam, options, rootASTName, processed);
}
break;
default:
type = '';
}
return type;
}
function declareNamedTypes(ast, options, rootASTName, processed = new Set()) {
if (processed.has(ast)) {
return '';
}
processed.add(ast);
switch (ast.type) {
case 'ARRAY':
return [
declareNamedTypes(ast.params, options, rootASTName, processed),
(0, AST_1.hasStandaloneName)(ast) ? generateStandaloneType(ast, options) : undefined,
]
.filter(Boolean)
.join('\n');
case 'ENUM':
return '';
case 'INTERFACE':
return getSuperTypesAndParams(ast)
.map(ast => (ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
declareNamedTypes(ast, options, rootASTName, processed))
.filter(Boolean)
.join('\n');
case 'INTERSECTION':
case 'TUPLE':
case 'UNION':
return [
(0, AST_1.hasStandaloneName)(ast) ? generateStandaloneType(ast, options) : undefined,
ast.params
.map(ast => declareNamedTypes(ast, options, rootASTName, processed))
.filter(Boolean)
.join('\n'),
'spreadParam' in ast && ast.spreadParam
? declareNamedTypes(ast.spreadParam, options, rootASTName, processed)
: undefined,
]
.filter(Boolean)
.join('\n');
default:
if ((0, AST_1.hasStandaloneName)(ast)) {
return generateStandaloneType(ast, options);
}
return '';
}
}
function generateTypeUnmemoized(ast, options) {
const type = generateRawType(ast, options);
if (options.strictIndexSignatures && ast.keyName === '[k: string]') {
return `${type} | undefined`;
}
return type;
}
exports.generateType = (0, lodash_1.memoize)(generateTypeUnmemoized);
function generateRawType(ast, options) {
(0, utils_1.log)('magenta', 'generator', ast);
if ((0, AST_1.hasStandaloneName)(ast)) {
return (0, utils_1.toSafeString)(ast.standaloneName);
}
switch (ast.type) {
case 'ANY':
return 'any';
case 'ARRAY':
return (() => {
const type = (0, exports.generateType)(ast.params, options);
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]';
})();
case 'BOOLEAN':
return 'boolean';
case 'INTERFACE':
return generateInterface(ast, options);
case 'INTERSECTION':
return generateSetOperation(ast, options);
case 'LITERAL':
return JSON.stringify(ast.params);
case 'NEVER':
return 'never';
case 'NUMBER':
return 'number';
case 'NULL':
return 'null';
case 'OBJECT':
return 'object';
case 'REFERENCE':
return ast.params;
case 'STRING':
return 'string';
case 'TUPLE':
return (() => {
const minItems = ast.minItems;
const maxItems = ast.maxItems || -1;
let spreadParam = ast.spreadParam;
const astParams = [...ast.params];
if (minItems > 0 && minItems > astParams.length && ast.spreadParam === undefined) {
// this is a valid state, and JSONSchema doesn't care about the item type
if (maxItems < 0) {
// no max items and no spread param, so just spread any
spreadParam = options.unknownAny ? AST_1.T_UNKNOWN : AST_1.T_ANY;
}
}
if (maxItems > astParams.length && ast.spreadParam === undefined) {
// this is a valid state, and JSONSchema doesn't care about the item type
// fill the tuple with any elements
for (let i = astParams.length; i < maxItems; i += 1) {
astParams.push(options.unknownAny ? AST_1.T_UNKNOWN : AST_1.T_ANY);
}
}
function addSpreadParam(params) {
if (spreadParam) {
const spread = '...(' + (0, exports.generateType)(spreadParam, options) + ')[]';
params.push(spread);
}
return params;
}
function paramsToString(params) {
return '[' + params.join(', ') + ']';
}
const paramsList = astParams.map(param => (0, exports.generateType)(param, options));
if (paramsList.length > minItems) {
/*
if there are more items than the min, we return a union of tuples instead of
using the optional element operator. This is done because it is more typesafe.
// optional element operator
type A = [string, string?, string?]
const a: A = ['a', undefined, 'c'] // no error
// union of tuples
type B = [string] | [string, string] | [string, string, string]
const b: B = ['a', undefined, 'c'] // TS error
*/
const cumulativeParamsList = paramsList.slice(0, minItems);
const typesToUnion = [];
if (cumulativeParamsList.length > 0) {
// actually has minItems, so add the initial state
typesToUnion.push(paramsToString(cumulativeParamsList));
}
else {
// no minItems means it's acceptable to have an empty tuple type
typesToUnion.push(paramsToString([]));
}
for (let i = minItems; i < paramsList.length; i += 1) {
cumulativeParamsList.push(paramsList[i]);
if (i === paramsList.length - 1) {
// only the last item in the union should have the spread parameter
addSpreadParam(cumulativeParamsList);
}
typesToUnion.push(paramsToString(cumulativeParamsList));
}
return typesToUnion.join('|');
}
// no max items so only need to return one type
return paramsToString(addSpreadParam(paramsList));
})();
case 'UNION':
return generateSetOperation(ast, options);
case 'UNKNOWN':
return 'unknown';
case 'CUSTOM_TYPE':
return ast.params;
}
}
/**
* Generate a Union or Intersection
*/
function generateSetOperation(ast, options) {
const members = ast.params.map(_ => (0, exports.generateType)(_, options));
const separator = ast.type === 'UNION' ? '|' : '&';
return members.length === 1 ? members[0] : '(' + members.join(' ' + separator + ' ') + ')';
}
function generateInterface(ast, options) {
return (`{` +
'\n' +
ast.params
.filter(_ => !_.isPatternProperty && !_.isUnreachableDefinition)
.map(({ isRequired, keyName, ast }) => [isRequired, keyName, ast, (0, exports.generateType)(ast, options)])
.map(([isRequired, keyName, ast, type]) => ((0, AST_1.hasComment)(ast) && !ast.standaloneName ? generateComment(ast.comment, ast.deprecated) + '\n' : '') +
escapeKeyName(keyName) +
(isRequired ? '' : '?') +
': ' +
type)
.join('\n') +
'\n' +
'}');
}
function generateComment(comment, deprecated) {
const commentLines = ['/**'];
if (deprecated) {
commentLines.push(' * @deprecated');
}
if (typeof comment !== 'undefined') {
commentLines.push(...comment.split('\n').map(_ => ' * ' + _));
}
commentLines.push(' */');
return commentLines.join('\n');
}
function generateStandaloneEnum(ast, options) {
const containsSpecialCharacters = (key) => /[^a-zA-Z0-9_]/.test(key);
return (((0, AST_1.hasComment)(ast) ? generateComment(ast.comment, ast.deprecated) + '\n' : '') +
'export ' +
(options.enableConstEnums ? 'const ' : '') +
`enum ${(0, utils_1.toSafeString)(ast.standaloneName)} {` +
'\n' +
ast.params
.map(({ ast, keyName }) => (containsSpecialCharacters(keyName) ? `"${keyName}"` : keyName) + ' = ' + (0, exports.generateType)(ast, options))
.join(',\n') +
'\n' +
'}');
}
function generateStandaloneInterface(ast, options) {
return (((0, AST_1.hasComment)(ast) ? generateComment(ast.comment, ast.deprecated) + '\n' : '') +
`export interface ${(0, utils_1.toSafeString)(ast.standaloneName)} ` +
(ast.superTypes.length > 0
? `extends ${ast.superTypes.map(superType => (0, utils_1.toSafeString)(superType.standaloneName)).join(', ')} `
: '') +
generateInterface(ast, options));
}
function generateStandaloneType(ast, options) {
return (((0, AST_1.hasComment)(ast) ? generateComment(ast.comment) + '\n' : '') +
`export type ${(0, utils_1.toSafeString)(ast.standaloneName)} = ${(0, exports.generateType)((0, lodash_1.omit)(ast, 'standaloneName') /* TODO */, options)}`);
}
function escapeKeyName(keyName) {
if (keyName.length && /[A-Za-z_$]/.test(keyName.charAt(0)) && /^[\w$]+$/.test(keyName)) {
return keyName;
}
if (keyName === '[k: string]') {
return keyName;
}
return JSON.stringify(keyName);
}
function getSuperTypesAndParams(ast) {
return ast.params.map(param => param.ast).concat(ast.superTypes);
}
//# sourceMappingURL=generator.js.map