@junaidatari/json2ts
Version:
Convert JSON objects to TypeScript interfaces automatically.
510 lines (509 loc) • 23 kB
TypeScript
/**
* 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;
}