Skip to content

Commit

Permalink
refactor: improve type safety
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Sep 26, 2024
1 parent 88c2803 commit 7f51a8a
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 40 deletions.
14 changes: 7 additions & 7 deletions src/generator/dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function extractTypeImports(declarations: string) {

export function generateTypes(schema: Schema, opts: GenerateTypesOptions = {}) {
opts = { ...GenerateTypesDefaults, ...opts };
const baseIden = " ".repeat(opts.indentation);
const baseIden = " ".repeat(opts.indentation || 0);
const interfaceCode =
`interface ${opts.interfaceName} {\n` +
_genTypes(schema, baseIden + " ", opts)
Expand Down Expand Up @@ -130,7 +130,7 @@ function _genTypes(
} else {
let type: string;
if (val.type === "array") {
type = `Array<${getTsType(val.items, opts)}>`;
type = `Array<${getTsType(val.items || [], opts)}>`;
} else if (val.type === "function") {
type = genFunctionType(val, opts);
} else {
Expand Down Expand Up @@ -182,25 +182,25 @@ function getTsType(
return type.type
.map((t) => {
// object is typed to an empty string by default, we need to type as object
if (t === "object" && type.type.length > 1) {
if (t === "object" && type.type!.length > 1) {
return `{\n` + _genTypes(type, " ", opts).join("\n") + `\n}`;
}
return TYPE_MAP[t];
})
.join("|");
}
if (type.type === "array") {
return `Array<${getTsType(type.items, opts)}>`;
return `Array<${getTsType(type.items || [], opts)}>`;
}
if (type.type === "object") {
return `{\n` + _genTypes(type, " ", opts).join("\n") + `\n}`;
}
return TYPE_MAP[type.type] || type.type;
}

export function genFunctionType(schema, opts: GenerateTypesOptions) {
export function genFunctionType(schema: Schema, opts: GenerateTypesOptions) {
return `(${genFunctionArgs(schema.args, opts)}) => ${getTsType(
schema.returns,
schema.returns || [],
opts,
)}`;
}
Expand Down Expand Up @@ -256,7 +256,7 @@ function generateJSDoc(schema: Schema, opts: GenerateTypesOptions): string[] {

for (const key in schema) {
if (!SCHEMA_KEYS.has(key)) {
buff.push("", `@${key} ${schema[key]}`);
buff.push("", `@${key} ${schema[key as keyof Schema] as string}`);
}
}

Expand Down
72 changes: 46 additions & 26 deletions src/loader/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

import { version } from "../../package.json";

type GetCodeFn = (loc: t.SourceLocation) => string;
type GetCodeFn = (loc: t.SourceLocation | null | undefined) => string;

const babelPluginUntyped: PluginItem = function (
api: ConfigAPI,
Expand Down Expand Up @@ -62,15 +62,18 @@ const babelPluginUntyped: PluginItem = function (
if (schemaProp && "value" in schemaProp) {
if (schemaProp.value.type === "ObjectExpression") {
// Object has $schema
schemaProp.value.properties.push(...astify(schema).properties);
schemaProp.value.properties.push(
...(astify(schema) as t.ObjectExpression).properties,
);
} else {
// Object has $schema which is not an object
// SKIP
}
} else {
// Object has not $schema
valueNode.properties.unshift(
...astify({ $schema: schema }).properties,
...(astify({ $schema: schema }) as t.ObjectExpression)
.properties,
);
}
} else {
Expand All @@ -95,7 +98,7 @@ const babelPluginUntyped: PluginItem = function (
// Experimental functions meta support
if (
!options.experimentalFunctions &&
!schema.tags.includes("@untyped")
!schema.tags?.includes("@untyped")
) {
return;
}
Expand All @@ -110,10 +113,13 @@ const babelPluginUntyped: PluginItem = function (

const _getLines = cachedFn(() => this.file.code.split("\n"));
const getCode: GetCodeFn = (loc) => {
if (!loc) {
return "";
}
const _lines = _getLines();
return (
_lines[loc.start.line - 1]
.slice(loc.start.column, loc.end.column)
?.slice(loc.start.column, loc.end.column)
.trim() || ""
);
};
Expand Down Expand Up @@ -143,7 +149,7 @@ const babelPluginUntyped: PluginItem = function (
arg,
mergedTypes(
arg,
inferAnnotationType(lparam.typeAnnotation, getCode),
inferAnnotationType(lparam.typeAnnotation!, getCode)!,
),
);
}
Expand Down Expand Up @@ -197,7 +203,7 @@ const babelPluginUntyped: PluginItem = function (
p.replaceWith(
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier(p.node.id.name),
t.identifier(p.node.id!.name),
astify({ $schema: schema }),
),
]),
Expand All @@ -220,7 +226,7 @@ function containsIncompleteCodeblock(line = "") {
function clumpLines(lines: string[], delimiters = [" "], separator = " ") {
const clumps: string[] = [];
while (lines.length > 0) {
const line = lines.shift();
const line = lines.shift()!;
if (
(line && !delimiters.includes(line[0]) && clumps.at(-1)) ||
containsIncompleteCodeblock(clumps.at(-1))
Expand Down Expand Up @@ -281,21 +287,25 @@ function parseJSDocs(input: string | string[]): Schema {
Object.assign(schema, getTypeDescriptor(type));
for (const typedef in typedefs) {
schema.markdownType = type;
schema.tsType = schema.tsType.replace(
new RegExp(typedefs[typedef], "g"),
typedef,
);
if (schema.tsType) {
schema.tsType = schema.tsType.replace(
new RegExp(typedefs[typedef], "g"),
typedef,
);
}
}
continue;
}
schema.tags.push(tag.trim());
schema.tags!.push(tag.trim());
}
}

return schema;
}

function astify(val) {
function astify(
val: unknown,
): t.Literal | t.Identifier | t.ArrayExpression | t.ObjectExpression {
if (typeof val === "string") {
return t.stringLiteral(val);
}
Expand All @@ -316,8 +326,17 @@ function astify(val) {
}
return t.objectExpression(
Object.getOwnPropertyNames(val)
.filter((key) => val[key] !== undefined && val[key] !== null)
.map((key) => t.objectProperty(t.identifier(key), astify(val[key]))),
.filter(
(key) =>
val[key as keyof typeof val] !== undefined &&
val[key as keyof typeof val] !== null,
)
.map((key) =>
t.objectProperty(
t.identifier(key),
astify(val[key as keyof typeof val]),
),
),
);
}

Expand All @@ -336,7 +355,7 @@ const AST_JSTYPE_MAP: Partial<Record<t.Expression["type"], JSType | "RegExp">> =

function inferArgType(e: t.Expression, getCode: GetCodeFn): TypeDescriptor {
if (AST_JSTYPE_MAP[e.type]) {
return getTypeDescriptor(AST_JSTYPE_MAP[e.type]);
return getTypeDescriptor(AST_JSTYPE_MAP[e.type]!);
}
if (e.type === "AssignmentExpression") {
return inferArgType(e.right, getCode);
Expand All @@ -351,7 +370,7 @@ function inferArgType(e: t.Expression, getCode: GetCodeFn): TypeDescriptor {
return {
type: "array",
items: {
type: normalizeTypes(itemTypes),
type: normalizeTypes(itemTypes as JSType[]),
},
};
}
Expand All @@ -361,22 +380,23 @@ function inferArgType(e: t.Expression, getCode: GetCodeFn): TypeDescriptor {
function inferAnnotationType(
ann: t.Identifier["typeAnnotation"],
getCode: GetCodeFn,
): TypeDescriptor | null {
if (ann.type !== "TSTypeAnnotation") {
return null;
): TypeDescriptor | undefined {
if (ann?.type !== "TSTypeAnnotation") {
return undefined;
}
return inferTSType(ann.typeAnnotation, getCode);
}

function inferTSType(
tsType: t.TSType,
getCode: GetCodeFn,
): TypeDescriptor | null {
function inferTSType(tsType: t.TSType, getCode: GetCodeFn): TypeDescriptor {
if (tsType.type === "TSParenthesizedType") {
return inferTSType(tsType.typeAnnotation, getCode);
}
if (tsType.type === "TSTypeReference") {
if ("name" in tsType.typeName && tsType.typeName.name === "Array") {
if (
tsType.typeParameters &&
"name" in tsType.typeName &&
tsType.typeName.name === "Array"
) {
return {
type: "array",
items: inferTSType(tsType.typeParameters.params[0], getCode),
Expand Down
2 changes: 1 addition & 1 deletion src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function resolveSchema(
root: obj,
defaults,
resolveCache: {},
ignoreDefaults: options.ignoreDefaults,
ignoreDefaults: !!options.ignoreDefaults,
});
// TODO: Create meta-schema fror superset of Schema interface
// schema.$schema = 'http://json-schema.org/schema#'
Expand Down
17 changes: 11 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,16 @@ export function mergedTypes(...types: TypeDescriptor[]): TypeDescriptor {
return types[0];
}
const tsTypes = normalizeTypes(
types.flatMap((t) => t.tsType).filter(Boolean),
types.flatMap((t) => t.tsType).filter(Boolean) as string[],
);
return {
type: normalizeTypes(types.flatMap((t) => t.type).filter(Boolean)),
type: normalizeTypes(
types.flatMap((t) => t.type).filter(Boolean) as JSType[],
),
tsType: Array.isArray(tsTypes) ? tsTypes.join(" | ") : tsTypes,
items: mergedTypes(...types.flatMap((t) => t.items).filter(Boolean)),
items: mergedTypes(
...(types.flatMap((t) => t.items).filter(Boolean) as TypeDescriptor[]),
),
};
}

Expand All @@ -126,16 +130,17 @@ export function normalizeTypes<T extends string>(val: T[]) {
return arr.length > 1 ? arr : arr[0];
}

export function cachedFn(fn) {
let val;
export function cachedFn<T>(fn: () => T): () => T {
let val: T | undefined;
let resolved = false;
return () => {
const cachedFn = () => {
if (!resolved) {
val = fn();
resolved = true;
}
return val;
};
return cachedFn as () => T;
}

const jsTypes: Set<JSType> = new Set([
Expand Down

0 comments on commit 7f51a8a

Please sign in to comment.