317 lines
9.7 KiB
TypeScript
317 lines
9.7 KiB
TypeScript
import { Type, Comment, Either, DefaultIntersect, Validation } from "./type";
|
|
import { TypeOf } from "./checks/type-of";
|
|
import { InstanceOf } from "./checks/instance-of";
|
|
import { Value } from "./checks/value";
|
|
import { Arr } from "./checks/array";
|
|
import { PartialStruct, Struct, MissingKey, OptionalKey, Dict, MergeIntersect } from "./checks/struct";
|
|
import { MapType } from "./checks/map";
|
|
import { SetType } from "./checks/set";
|
|
import { Any } from "./checks/any";
|
|
import { Is } from "./checks/is";
|
|
import { Never } from "./checks/never";
|
|
import { Kind } from "./kind";
|
|
|
|
type ToTypescriptOpts = {
|
|
useReference?: {
|
|
[ref: string]: Type<any>,
|
|
},
|
|
|
|
indent: string,
|
|
indentLevel: number,
|
|
};
|
|
|
|
export type TypescriptUserOpts = Partial<ToTypescriptOpts> & {
|
|
assignToType?: string,
|
|
};
|
|
|
|
type SingleConversionWithOpts = [ type: Kind, userOpts: TypescriptUserOpts ];
|
|
type SingleConversion = [ type: Kind ];
|
|
type MultipleConversion = [ types: { [name: string]: Kind } ];
|
|
|
|
export function toTypescript(...args: SingleConversion | SingleConversionWithOpts | MultipleConversion): string {
|
|
if(args.length === 2) {
|
|
const [ type, userOpts ] = args;
|
|
const opts = Object.assign({ indent: " ", indentLevel: 0 }, userOpts);
|
|
// assignToType is only valid at the top level, so delete it if it exists
|
|
delete opts.assignToType;
|
|
|
|
const ts = toTS(type, opts);
|
|
|
|
if(userOpts.assignToType) return `type ${userOpts.assignToType} = ${ts};`;
|
|
return ts;
|
|
}
|
|
|
|
const arg = args[0]
|
|
if(arg instanceof Type) return toTypescript(arg, {});
|
|
|
|
const keys = Object.keys(arg);
|
|
if(keys.length === 0) return "";
|
|
const output: string[] = [];
|
|
for(const key of keys) {
|
|
const refs = Object.assign({}, arg);
|
|
delete refs[key];
|
|
output.push(toTypescript(arg[key], {
|
|
useReference: refs,
|
|
assignToType: key,
|
|
}));
|
|
}
|
|
return output.join("\n\n");
|
|
}
|
|
|
|
function toTS(type: Kind, opts: ToTypescriptOpts): string {
|
|
if(opts.useReference) {
|
|
for(const key in opts.useReference) {
|
|
const val = opts.useReference[key];
|
|
if(val === type) return key;
|
|
}
|
|
}
|
|
|
|
if(type instanceof Comment) return fromComment(type, opts);
|
|
if(type instanceof Either) return fromEither(type, opts);
|
|
if(type instanceof DefaultIntersect) return fromIntersect(type, opts);
|
|
if(type instanceof MergeIntersect) return fromIntersect(type, opts);
|
|
if(type instanceof Validation) return fromValidation();
|
|
if(type instanceof TypeOf) return fromTypeof(type);
|
|
if(type instanceof InstanceOf) return fromInstanceOf(type);
|
|
if(type instanceof Value) return fromValue(type);
|
|
if(type instanceof Arr) return fromArr(type, opts);
|
|
if(type instanceof Struct) return fromStruct(type, opts);
|
|
if(type instanceof Dict) return fromDict(type, opts);
|
|
if(type instanceof MapType) return fromMap(type, opts);
|
|
if(type instanceof SetType) return fromSet(type, opts);
|
|
if(type instanceof Any) return fromAny();
|
|
if(type instanceof Is) return fromIs(type);
|
|
if(type instanceof PartialStruct) return fromPartial(type, opts);
|
|
return fromNever(type);
|
|
}
|
|
|
|
function fromPartial(p: PartialStruct<any>, opts: ToTypescriptOpts) {
|
|
return `Partial<${toTS(p.struct, opts)}>`;
|
|
}
|
|
|
|
function fromComment(c: Comment<any>, opts: ToTypescriptOpts) {
|
|
const i = indent(opts);
|
|
const commentLines = formatCommentString(c.commentStr, opts);
|
|
return `${commentLines}\n${i}${toTS(c.wrapped, opts)}`;
|
|
}
|
|
|
|
function formatCommentString(commentStr: string, opts: ToTypescriptOpts) {
|
|
const i = indent(opts);
|
|
|
|
const commentLines = commentStr.split("\n").map(line => {
|
|
return line.trim();
|
|
}).filter(line => line !== "");
|
|
|
|
if(commentLines.length === 0) return "";
|
|
if(commentLines.length === 1) {
|
|
return `// ${commentLines[0]}`;
|
|
}
|
|
|
|
const lines = [ '/*' ]
|
|
for(const line of commentLines) {
|
|
lines.push(`${i} * ${line.trim()}`);
|
|
}
|
|
lines.push(`${i} */`);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
// TODO: Should either strip immediate-child comments?
|
|
function fromEither(e: Either<any, any>, opts: ToTypescriptOpts) {
|
|
const i = indent(opts);
|
|
return [
|
|
toTS(e.l, opts),
|
|
`${i}${opts.indent}| ${toTS(e.r, {...opts, indentLevel: opts.indentLevel + 1})}`,
|
|
].join("\n");
|
|
}
|
|
|
|
// TODO: Should intersect strip immediate-child comments?
|
|
function fromIntersect(
|
|
i: DefaultIntersect<any, any> | MergeIntersect<any, any, any, any>,
|
|
opts: ToTypescriptOpts,
|
|
) {
|
|
// Handle validations chained with actual TS types, converting them to comments
|
|
if(i.l instanceof Validation) return toTS(new Comment(i.l.desc, i.r), opts);
|
|
if(i.r instanceof Validation) return toTS(new Comment(i.r.desc, i.l), opts);
|
|
|
|
const indentation = indent(opts);
|
|
return [
|
|
toTS(i.l, opts),
|
|
`${indentation}${opts.indent}& ${toTS(i.r, {...opts, indentLevel: opts.indentLevel + 1})}`,
|
|
].join("\n");
|
|
}
|
|
|
|
function fromValidation(): string {
|
|
throw new Error(
|
|
"Can't convert arbitrary validation functions to TypeScript types; make sure to .and() it " +
|
|
"with a valid type, and it will be converted into a comment above the type"
|
|
);
|
|
}
|
|
|
|
function fromTypeof(t: TypeOf<any>): string {
|
|
switch(t.typestring) {
|
|
case "undefined": return t.typestring;
|
|
case "object": return "Object";
|
|
case "boolean": return t.typestring;
|
|
case "number": return t.typestring;
|
|
case "string": return t.typestring;
|
|
case "symbol": return "Symbol";
|
|
case "function": return "Function";
|
|
case "bigint": return "bigint";
|
|
}
|
|
}
|
|
|
|
function fromInstanceOf(i: InstanceOf<any>) {
|
|
if(!i.klass.name) throw new Error("Can't convert anonymous classes to TypeScript");
|
|
return `${i.klass.name}`;
|
|
}
|
|
|
|
function fromValue(v: Value<any>) {
|
|
const vType = typeof v.val;
|
|
if(vType !== "string" && vType !== "number" && vType !== "boolean" && v.val !== null && v.val !== undefined) {
|
|
throw new Error(
|
|
"Only string, numeric, undefined, boolean, and null value types can be auto-converted to TypeScript"
|
|
);
|
|
}
|
|
if(vType === "string") return JSON.stringify(v.val);
|
|
|
|
return `${v.val}`;
|
|
}
|
|
|
|
function fromArr(a: Arr<any>, opts: ToTypescriptOpts) {
|
|
return `Array<${toTS(a.elementType, opts)}>`;
|
|
}
|
|
|
|
function fromStruct(s: Struct<any>, opts: ToTypescriptOpts) {
|
|
const lines = [ "{" ];
|
|
const keyOpts = {
|
|
...opts,
|
|
indentLevel: opts.indentLevel + 1,
|
|
};
|
|
const keyIndent = indent(keyOpts);
|
|
const keys = Object.keys(s.definition);
|
|
|
|
for(let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
const keyType = [ key ];
|
|
const val = s.definition[key];
|
|
if(val instanceof MissingKey) {
|
|
throw new Error(
|
|
"missing(...) fields can't be represented in TypeScript; consider using optional(...)"
|
|
);
|
|
}
|
|
if(val instanceof OptionalKey) keyType.push("?");
|
|
keyType.push(": ");
|
|
const stripped = stripOuterComments(val);
|
|
if(stripped.comments.length > 0) {
|
|
// Visually separate the start of a commented field unless it's the first field
|
|
if(i !== 0) {
|
|
// But don't double-newline if you already separated the last line
|
|
if(lines[lines.length - 1] !== "") lines.push("");
|
|
}
|
|
// Put the comment on the line above the key
|
|
lines.push(keyIndent + formatCommentString(stripped.comments.join("\n"), keyOpts));
|
|
}
|
|
keyType.push(toTS(stripped.inner, keyOpts));
|
|
keyType.push(",");
|
|
lines.push(keyIndent + keyType.join(""));
|
|
|
|
// Visually separate the end of a commented field, unless it's the last field
|
|
if(stripped.comments.length > 0 && i !== keys.length - 1) lines.push("");
|
|
}
|
|
lines.push(indent(opts) + "}");
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
type StrippedComments = {
|
|
comments: string[],
|
|
inner: Kind,
|
|
};
|
|
|
|
function stripOuterComments(t: Kind | OptionalKey<any>): StrippedComments {
|
|
if(t instanceof OptionalKey) return stripOuterComments(t.type);
|
|
if(t instanceof Comment) {
|
|
const inner = stripOuterComments(t.wrapped);
|
|
return {
|
|
comments: [ t.commentStr, ...inner.comments ],
|
|
inner: inner.inner,
|
|
}
|
|
}
|
|
|
|
const algebra = [ DefaultIntersect, MergeIntersect, Either ] as const;
|
|
|
|
for(const al of algebra) {
|
|
if(t instanceof al) {
|
|
if(t.l instanceof Validation) {
|
|
return stripOuterComments(new Comment(t.l.desc, t.r));
|
|
}
|
|
if(t.r instanceof Validation) {
|
|
return stripOuterComments(new Comment(t.r.desc, t.l));
|
|
}
|
|
if(t.l instanceof Comment) {
|
|
return stripOuterComments(handleStripAlgebra(t.l.commentStr, t.l.wrapped, t.r, al));
|
|
}
|
|
if(t.r instanceof Comment) {
|
|
return stripOuterComments(handleStripAlgebra(t.r.commentStr, t.l, t.r.wrapped, al));
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
comments: [],
|
|
inner: t,
|
|
};
|
|
}
|
|
|
|
function handleStripAlgebra(
|
|
desc: string,
|
|
r: Type<any>,
|
|
l: Type<any>,
|
|
constr: { new(r: any, l: any): Type<any> }
|
|
): Type<any> {
|
|
return new Comment(desc, new constr(r, l));
|
|
}
|
|
|
|
function fromDict(d: Dict<any>, opts: ToTypescriptOpts) {
|
|
const i = indent(opts);
|
|
const valString = toTS(d.valueType, {...opts, indentLevel: opts.indentLevel + 1});
|
|
|
|
// For single-line values, return a single-line dict
|
|
if(valString.indexOf("\n") < 0) return `{[${d.namedKey}: string]: ${valString}}`;
|
|
|
|
// For multiline values, return a multiline dict
|
|
return [
|
|
"{",
|
|
`${i}${opts.indent}[${d.namedKey}: string]: ${valString}`,
|
|
`${i}}`,
|
|
].join("\n");
|
|
}
|
|
|
|
function fromMap(m: MapType<any, any>, opts: ToTypescriptOpts) {
|
|
const keyString = toTS(m.keyType, opts);
|
|
const valString = toTS(m.valueType, opts);
|
|
return `Map<${keyString}, ${valString}>`;
|
|
}
|
|
|
|
function fromSet(s: SetType<any>, opts: ToTypescriptOpts) {
|
|
const valString = toTS(s.valueType, opts);
|
|
return `Set<${valString}>`;
|
|
}
|
|
|
|
function fromAny() {
|
|
return "any";
|
|
}
|
|
|
|
function fromIs(i: Is<any>) {
|
|
return i.name;
|
|
}
|
|
|
|
// Despite not using the type, we take it to help ensure exhaustiveness checking in the toTS fn
|
|
function fromNever(_: Never) {
|
|
return "never";
|
|
}
|
|
|
|
function indent(opts: ToTypescriptOpts) {
|
|
return opts.indent.repeat(opts.indentLevel);
|
|
}
|