UNPKG

@junaidatari/json2ts

Version:

Convert JSON objects to TypeScript interfaces automatically.

510 lines (509 loc) 23 kB
/** * Utility class containing helper methods for JSON to TypeScript conversion. * Provides type detection, validation, and formatting functionality for converting * JSON data into TypeScript interfaces with various configuration options. * * Features include: * - Type detection from arrays and objects with configurable tuple generation * - Interface name suggestion based on JSON structure analysis * - Property name validation and formatting with case transformation support * - Robust JSON parsing with comprehensive error handling and reporting * - TypeScript identifier validation to avoid reserved words * * @author Junaid Atari <mj.atari@gmail.com> * @copyright 2025 Junaid Atari * @see https://github.com/blacksmoke26 */ import type { ConvertOptions, ParseResult } from '../typings/global'; /** * Error types for JSON parsing failures. */ export declare enum JsonParseError { INVALID_INPUT = "Invalid input: provided value cannot be parsed", INVALID_FORMAT = "Invalid JSON format: input does not appear to be valid JSON", PARSE_FAILED = "JSON parsing failed", UNDEFINED_RESULT = "Invalid JSON: parsed result is undefined" } /** * Utility class containing helper methods for JSON to TypeScript conversion. * Provides type detection and analysis functionality for array values. */ export default abstract class ConverterUtils { /** * Set of TypeScript reserved words that cannot be used as identifiers. * Includes all JavaScript keywords plus TypeScript-specific keywords. * Used for validation when generating TypeScript interface property names. */ private static readonly TS_RESERVED_WORDS; private static readonly BUILT_IN_TYPES; /** * Set of characters that can appear at the beginning of a valid JSON string. * Used for quick validation to determine if a string might be valid JSON * before attempting to parse it. * * The characters are: * - '{' and '[': Object and array start * - '"': String start * - 't': true literal * - 'f': false literal * - 'n': null literal * - '-': Negative number start * - '0'-'9': Digit characters for numbers */ private static readonly JSON_START_CHARS; /** * Detects the TypeScript type from an array of values. * Creates a tuple type if mixed values are present within the configured size limits. * * This method analyzes array contents to determine the most appropriate TypeScript type: * - For arrays with identical primitive types: returns array type (e.g., "number[]") * - For mixed types within size limits: returns tuple type (e.g., "[number, string]") * - For arrays outside size limits: returns generic type (e.g., "any[]") * - Handles special values like null, undefined, objects, and nested arrays * * @param values - The array of values to analyze * @param maxTupleSize - Maximum number of items to convert to tuple type. * If array length exceeds this, returns array type instead. * @default 10 * @param minTupleSize - Minimum number of items required to create a tuple type. * If array length is less than this, returns array type instead. * @default 2 * @returns A string representing the detected TypeScript type * * @example * ```typescript * // Same type array * ConverterUtils.detectTypeFromArray([1, 2, 3]) // returns "number[]" * * // Mixed types within tuple size limits * ConverterUtils.detectTypeFromArray([1, "two", true]) // returns "[number, string, boolean]" * * // Large array - returns generic type * ConverterUtils.detectTypeFromArray(Array(15).fill(0)) // returns "number[]" * * // Array with objects * ConverterUtils.detectTypeFromArray([{a: 1}, {b: 2}]) // returns "object[]" * * // Mixed with null and undefined * ConverterUtils.detectTypeFromArray([1, null, "str"]) // returns "[number, null, string]" * ``` */ static detectTypeFromArray(values: unknown[], maxTupleSize?: number, minTupleSize?: number): string; /** * Detects the TypeScript type from a JavaScript object or value (Version 2). * An improved version of `detectJsTypeFromObject` with better organization and performance. * * This method performs comprehensive type detection including: * - Primitive types (string, number, boolean, bigint, symbol) * - Special values (null, undefined) * - Built-in objects (Date, RegExp, Error, Promise, etc.) * - Typed arrays and array buffers * - Functions (including async functions) * - Custom class instances * - Iterables and generators * * @param obj - The object or value to analyze for type detection * @param strict - Whether to use strict typing. When true, returns 'unknown' for * ambiguous cases instead of 'any'. @default false * @returns The detected TypeScript type as a string, or null if the type * cannot be determined (typically for plain objects that should be * handled separately) * * @example * ```typescript * // Primitive types * ConverterUtils.detectJsTypeFromObject("hello") // returns "string" * ConverterUtils.detectJsTypeFromObject(42) // returns "number" * ConverterUtils.detectJsTypeFromObject(true) // returns "boolean" * * // Special values * ConverterUtils.detectJsTypeFromObject(null) // returns "unknown" * ConverterUtils.detectJsTypeFromObject(undefined) // returns "unknown" * ConverterUtils.detectJsTypeFromObject(null, true) // returns "null" * * // Built-in objects * ConverterUtils.detectJsTypeFromObject(new Date()) // returns "Date" * ConverterUtils.detectJsTypeFromObject(/pattern/) // returns "RegExp" * ConverterUtils.detectJsTypeFromObject(new Error()) // returns "Error" * * // Arrays and typed arrays * ConverterUtils.detectJsTypeFromObject([1, 2, 3]) // returns "any[]" * ConverterUtils.detectJsTypeFromObject(new Uint8Array()) // returns "Uint8Array" * * // Functions * ConverterUtils.detectJsTypeFromObject(() => {}) // returns "(...args: any[]) => any" * ConverterUtils.detectJsTypeFromObject(async () => {}) // returns "(...args: any[]) => Promise<any>" * * // Custom class instances * class MyClass {} * ConverterUtils.detectJsTypeFromObject(new MyClass()) // returns "MyClass" * * // Plain objects return null (should be handled separately) * ConverterUtils.detectJsTypeFromObject({a: 1}) // returns null * ``` */ static detectJsTypeFromObject(obj: any, strict?: boolean): string | null; /** * Suggests a meaningful interface name based on the provided JSON data. * Analyzes the structure and content of JSON objects to generate * appropriate interface names that reflect the data's purpose and structure. * * @param jsonData - The JSON data to analyze for interface naming * @param defaultName - Fallback name to use when no suitable name can be derived * @returns A suggested interface name based on the JSON structure * * @example * ```typescript * // Simple object * ConverterUtils.suggestInterfaceName({ name: "John", age: 30 }) // returns "Person" * * // Object with array property * ConverterUtils.suggestInterfaceName({ users: [{ id: 1 }] }) // returns "UserList" * * // Nested object * ConverterUtils.suggestInterfaceName({ profile: { name: "John" } }) // returns "Profile" * * // Array of objects * ConverterUtils.suggestInterfaceName([{ id: 1 }, { id: 2 }]) // returns "Item" * * // Empty object * ConverterUtils.suggestInterfaceName({}) // returns "RootObject" * ``` */ static suggestInterfaceName(jsonData: unknown, defaultName?: string): string; /** * Validates and corrects a TypeScript interface property name. * Property names with spaces or not starting with [a-z] will be quoted. * @param key - The property name to validate and correct * @returns A valid TypeScript property name (quoted if necessary) */ static suggestPropertyName(key: string, fallback?: string): string; /** * Parses JSON string or returns the input if it's already an object. * Enhanced with comprehensive validation and detailed error reporting. * * This method provides robust JSON parsing with the following features: * - Accepts JSON strings, objects, or null/undefined values * - Performs input validation before parsing attempts * - Provides detailed error messages with position information * - Returns structured results with error categorization * - Handles edge cases like empty strings and whitespace * - Includes input snippets in error messages for debugging * * The parsing process includes multiple validation stages: * 1. Null/undefined check with immediate error reporting * 2. Type detection (string vs already parsed object) * 3. Empty/whitespace string validation * 4. Quick format validation using first character check * 5. Actual JSON parsing with comprehensive error handling * * Error types returned: * - INVALID_INPUT: Null, undefined, empty, or whitespace input * - INVALID_FORMAT: Input doesn't start with valid JSON character * - PARSE_FAILED: JSON.parse() throws an exception * - UNDEFINED_RESULT: Parsed result is undefined (edge case) * * @param json The JSON string, object, or null/undefined value to parse. * When a non-string value is provided, it's returned as-is. * When a string is provided, it undergoes validation and parsing. * * @returns ParseResult object containing: * - data: The parsed JSON object/array/primitive, or null if parsing failed * - error: JsonParseError enum value if parsing failed, undefined otherwise * - details: Human-readable error message with context for debugging, * including position information and input snippets when applicable * * @example * ```typescript * // Valid JSON string * const result1 = ConverterUtils.jsonParse('{"name": "John"}'); * // returns: { data: { name: "John" } } * * // Already parsed object * const result2 = ConverterUtils.jsonParse({ name: "John" }); * // returns: { data: { name: "John" } } * * // Invalid JSON * const result3 = ConverterUtils.jsonParse('{"name": "John"'); * // returns: { * // data: null, * // error: JsonParseError.PARSE_FAILED, * // details: "at position 15: Unexpected end of JSON input\nInput: {\"name\": \"John\"" * // } * * // Empty string * const result4 = ConverterUtils.jsonParse(' '); * // returns: { * data: null, * error: JsonParseError.INVALID_INPUT, * details: 'Input is empty or whitespace' * } * * // Invalid format * const result5 = ConverterUtils.jsonParse('hello world'); * // returns: { * data: null, * error: JsonParseError.INVALID_FORMAT, * details: "Input starts with 'h', expected JSON value" * } * ``` */ static jsonParse(json: string | unknown | null): ParseResult; /** * Formats a property declaration string for TypeScript interfaces. * * This method takes a property name and type along with optional formatting options * to generate a properly formatted TypeScript property declaration. It handles: * - Property name validation and quoting when necessary * - Application of case transformation based on propertyCase option * - Addition of readonly modifier when readonlyProperties is enabled * - Addition of optional modifier (?) when optionalProperties is enabled * * The resulting string is ready to be used directly in a TypeScript interface definition. * * @param property The original property name from the JSON data. * @param type The detected or specified TypeScript type for the property. * @param options Configuration options that control the formatting behavior: * - propertyCase: Case transformation for the property name * - readonlyProperties: Whether to add readonly modifier * - optionalProperties: Whether to make the property optional * @returns A formatted TypeScript property declaration string ready for interface definition. * * @example * ```typescript * // Basic usage * ConverterUtils.formatPropertyValue('name', 'string'); * // returns: "name: string" * * // With readonly and optional * ConverterUtils.formatPropertyValue('age', 'number', { * readonlyProperties: true, * optionalProperties: true * }); * // returns: "readonly age?: number" * * // With property case transformation * ConverterUtils.formatPropertyValue('user_name', 'string', { * propertyCase: 'camel' * }); * // returns: "userName: string" * * // With special characters in property name * ConverterUtils.formatPropertyValue('full-name', 'string'); * // returns: '"full-name": string' * ``` */ static formatPropertyValue(property: string, type: string, options?: ConvertOptions): string; /** * Validates if a string is a valid TypeScript identifier. * * A valid TypeScript identifier must: * - Start with a letter, underscore, or dollar sign * - Contain only letters, numbers, underscores, or dollar signs * - Not be a reserved TypeScript keyword * - Not consist solely of underscores or dollar signs * - Not start with a digit * * @param name - The string to validate as a TypeScript identifier * @returns true if the string is a valid identifier, false otherwise * * @example * ```typescript * ConverterUtils.checkIdentifier('validName'); // true * ConverterUtils.checkIdentifier('_private'); // true * ConverterUtils.checkIdentifier('$special'); // true * ConverterUtils.checkIdentifier('123invalid'); // false * ConverterUtils.checkIdentifier('class'); // false (reserved word) * ConverterUtils.checkIdentifier(''); // false (empty) * ConverterUtils.checkIdentifier('___'); // false (only special chars) * ``` */ static checkIdentifier(name: string): boolean; /** * Validates if a string can be safely used as a TypeScript type name. * * A valid type name must: * - Start with an uppercase letter (PascalCase convention) * - Contain only letters and numbers * - Not be a reserved TypeScript keyword * - Not be a built-in type name * - Not conflict with common library types * * @param name - The string to validate as a TypeScript type name * @returns true if the string is a valid type name, false otherwise * * @example * ```typescript * ConverterUtils.checkTypeName('User'); // true * ConverterUtils.checkTypeName('UserProfile'); // true * ConverterUtils.checkTypeName('user'); // false (not PascalCase) * ConverterUtils.checkTypeName('String'); // false (built-in type) * ConverterUtils.checkTypeName('123Invalid'); // false (starts with number) * ``` */ static checkTypeName(name: string): boolean; /** * Converts a string to a valid TypeScript interface name. * Applies various transformations to ensure the name follows TypeScript conventions. * * @param name - The input string to convert to an interface name * @param fallback - Default name to use if input is invalid or empty * @returns A valid TypeScript interface name in PascalCase * * @example * ```typescript * // Basic transformations * ConverterUtils.toInterfaceName('user_profile'); // "UserProfile" * ConverterUtils.toInterfaceName('user-name'); // "UserName" * ConverterUtils.toInterfaceName('firstName'); // "FirstName" * ConverterUtils.toInterfaceName('$variable'); // "$Variable" * * // Edge cases and validation * ConverterUtils.toInterfaceName('123invalid'); // "RootObject" * ConverterUtils.toInterfaceName(''); // "RootObject" * ConverterUtils.toInterfaceName(null); // "RootObject" * ConverterUtils.toInterfaceName(undefined); // "RootObject" * * // Reserved words handling * ConverterUtils.toInterfaceName('class'); // "Class" * ConverterUtils.toInterfaceName('interface'); // "Interface" * ConverterUtils.toInterfaceName('string'); // "String" * * // Complex patterns * ConverterUtils.toInterfaceName('user_profile_name'); // "UserProfileName" * ConverterUtils.toInterfaceName('test_case_123_value'); // "TestCase123Value" * ConverterUtils.toInterfaceName('prop@#$%name'); // "Propname" * ConverterUtils.toInterfaceName('a_b_c_d_e'); // "ABCDE" * * // Unicode and special characters * ConverterUtils.toInterfaceName('café'); // "Café" * ConverterUtils.toInterfaceName('naïve'); // "Naïve" * ConverterUtils.toInterfaceName('пользователь'); // "Пользователь" * * // Leading/trailing special characters * ConverterUtils.toInterfaceName('_property'); // "_Property" * ConverterUtils.toInterfaceName('$variable'); // "$Variable" * ConverterUtils.toInterfaceName('property_'); // "Property" * ConverterUtils.toInterfaceName('__property__'); // "__Property" * * // Numbers in names * ConverterUtils.toInterfaceName('test123'); // "Test123" * ConverterUtils.toInterfaceName('123test'); // "RootObject" * ConverterUtils.toInterfaceName('test_123_case'); // "Test123Case" * ``` */ static toInterfaceName(name: string, fallback?: string): string; /** * Determines if a value should be treated as a date type. * Checks for various date string formats and Date objects. * * @param value - The value to check for date type * @returns true if the value should be treated as a date, false otherwise * * @example * ```typescript * ConverterUtils.isDateType(new Date()); // true * ConverterUtils.isDateType('2023-01-01'); // true * ConverterUtils.isDateType('2023-01-01T00:00:00Z'); // true * ConverterUtils.isDateType('not a date'); // false * ConverterUtils.isDateType(123); // false * ``` */ static isDateType(value: unknown): boolean; /** * Determines if a value should be treated as an enum type. * Checks if an array contains only string values that could be enum candidates. * * @param value - The value to check for enum type * @returns true if the value should be treated as an enum, false otherwise * * @example * ```typescript * ConverterUtils.isEnumType(['RED', 'GREEN', 'BLUE']); // true * ConverterUtils.isEnumType([1, 2, 3]); // false (numbers) * ConverterUtils.isEnumType(['red', 'green', 'blue']); // true * ConverterUtils.isEnumType(['not', 'valid', 123]); // false (mixed types) * ``` */ static isEnumType(value: unknown): boolean; /** * Infers the most specific type for a value. * Combines multiple type detection methods for comprehensive type inference. * * @param value - The value to analyze * @param options - Configuration options for type detection * @returns The most specific TypeScript type that can be inferred * * @example * ```typescript * ConverterUtils.inferType('2023-01-01'); // "Date" * ConverterUtils.inferType(['RED', 'GREEN']); // "'RED' | 'GREEN'" * ConverterUtils.inferType({}); // null (should be handled as object) * ConverterUtils.inferType([1, 2, 3]); // "number[]" * ``` */ static inferType(value: unknown, options?: ConvertOptions): string | null; /** * Generates a unique type name that doesn't conflict with existing names. * Appends a number suffix if the name already exists in the provided set. * * @param baseName - The desired base name for the type * @param existingNames - Set of already used type names to avoid conflicts * @param suffix - Optional suffix to append (defaults to "Type") * @returns A unique type name that doesn't conflict with existing names * * @example * ```typescript * const used = new Set(['User', 'UserType']); * ConverterUtils.generateUniqueName('User', used); // "UserType2" * ConverterUtils.generateUniqueName('Profile', used); // "ProfileType" * ``` */ static generateUniqueName(baseName: string, existingNames: Set<string>, suffix?: string): string; /** * Analyzes an object's structure to determine if it's suitable for tuple conversion. * Checks if an object has numeric keys that could represent array indices. * * @param obj - The object to analyze * @returns true if the object structure suggests it should be a tuple, false otherwise * * @example * ```typescript * ConverterUtils.isTupleLike({ 0: 'a', 1: 'b', 2: 'c' }); // true * ConverterUtils.isTupleLike({ 0: 'a', 2: 'b' }); // false (missing index) * ConverterUtils.isTupleLike({ a: 'x', b: 'y' }); // false (non-numeric keys) * ``` */ static isTupleLike(obj: Record<string, unknown>): boolean; /** * Converts a tuple-like object to an array representation. * Transforms an object with numeric keys into a proper array. * * @param obj - The tuple-like object to convert * @returns An array with values ordered by their numeric keys * * @example * ```typescript * ConverterUtils.tupleLikeToArray({ 0: 'a', 1: 'b', 2: 'c' }); * // returns ['a', 'b', 'c'] * ``` */ static tupleLikeToArray(obj: Record<string, unknown>): unknown[]; /** * Checks if a type name represents a primitive type. * Useful for determining if a type needs interface generation. * * @param typeName - The type name to check * @returns true if the type is a primitive type, false otherwise * * @example * ```typescript * ConverterUtils.isPrimitiveType('string'); // true * ConverterUtils.isPrimitiveType('number'); // true * ConverterUtils.isPrimitiveType('boolean'); // true * ConverterUtils.isPrimitiveType('CustomType'); // false * ConverterUtils.isPrimitiveType('string[]'); // false (array) * ``` */ static isPrimitiveType(typeName: string): boolean; }