kanji-inspector
Version:
A TypeScript library that provides Unihan data as type-safe constants for Kanji and CJK ideographs.
331 lines (330 loc) • 12.6 kB
JavaScript
class TypeGen {
rootTypeName;
indentSize;
indent;
arrayElementTypeName;
generatedTypes;
constructor(options = {}) {
this.rootTypeName = options.rootTypeName ?? 'RootType';
this.indentSize = options.indentSize ?? 2;
this.arrayElementTypeName = options.arrayElementTypeName ?? 'ContentType';
this.indent = ' '.repeat(this.indentSize);
this.generatedTypes = new Map();
}
/**
* プロパティ名から型名を生成します
*/
getTypeNameFromProperty(propertyName) {
const capitalized = propertyName.charAt(0).toUpperCase() + propertyName.slice(1);
return `${capitalized}Type`;
}
/**
* 配列要素の型名を生成します
*/
getArrayElementTypeName() {
return this.arrayElementTypeName;
}
/**
* 値の型を判定します
*/
getPropertyType(value, propertyName) {
if (value === null || value === undefined) {
return 'any';
}
if (Array.isArray(value)) {
if (value.length === 0)
return 'any[]';
// 配列内のすべての要素がオブジェクトかどうかチェック
const allObjectElements = value.every(v => typeof v === 'object' && v !== null && !Array.isArray(v));
if (allObjectElements && propertyName) {
// すべての要素がオブジェクトの場合、共通プロパティから型を生成
const elementTypeName = this.arrayElementTypeName;
const commonProps = this.extractCommonProperties(value);
const typeDefinition = this.generateTypeDefinition(commonProps, elementTypeName);
this.generatedTypes.set(elementTypeName, typeDefinition);
return `${elementTypeName}[]`;
}
else {
// 従来の方法:要素の型をすべて調べる
const elementTypes = Array.from(new Set(value.map(v => this.getPropertyType(v))));
if (elementTypes.length === 1) {
const baseType = elementTypes[0];
return `${baseType}[]`;
}
else {
return 'any[]';
}
}
}
if (typeof value === 'object') {
if (propertyName) {
const typeName = this.getTypeNameFromProperty(propertyName);
const typeDefinition = this.generateTypeDefinition(this.extractProperties(value), typeName);
this.generatedTypes.set(typeName, typeDefinition);
return typeName;
}
// プロパティ名がない場合は汎用的な型名を生成
return 'any'; // プロパティ名なしの場合はanyとして扱う
}
return typeof value;
}
/**
* オブジェクトからプロパティの型情報を抽出します
*/
extractProperties(obj) {
return Object.entries(obj).map(([name, value]) => ({
name,
type: this.getPropertyType(value, name),
optional: false
}));
}
/**
* 配列から共通のプロパティを抽出し、オプショナルなプロパティを特定します
*/
extractCommonProperties(array) {
const allProperties = new Map();
// すべてのオブジェクトのプロパティを収集
array.forEach(obj => {
// objがオブジェクトかどうかチェック
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
Object.entries(obj).forEach(([name, value]) => {
if (!allProperties.has(name)) {
allProperties.set(name, new Set());
}
allProperties.get(name)?.add(this.getPropertyType(value, name));
});
}
});
// 共通プロパティを生成
return Array.from(allProperties.entries()).map(([name, types]) => {
const typeSet = Array.from(types);
const type = typeSet.length > 1
? typeSet.join(' | ')
: typeSet[0];
// objがオブジェクトの場合のみチェック
const existsInAllObjects = array.every(obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj) && name in obj);
// 全てのオブジェクトに存在する場合は必須プロパティとする
const optional = !existsInAllObjects;
return {
name,
type,
optional
};
});
}
/**
* 型定義を文字列として生成します
*/
generateTypeDefinition(properties, typeName) {
const propertiesStr = properties
.map(prop => `${this.indent}${prop.name}${prop.optional ? '?' : ''}: ${prop.type};`)
.join('\n');
return `type ${typeName} = {\n${propertiesStr}\n};`;
}
/**
* データからTypeScriptの型定義を生成します
*/
generate(data) {
this.generatedTypes.clear();
if (Array.isArray(data)) {
if (data.length === 0) {
return `type ${this.rootTypeName} = any[];`;
}
const properties = this.extractCommonProperties(data);
const elementTypeName = this.getArrayElementTypeName();
const elementType = this.generateTypeDefinition(properties, elementTypeName);
const allTypes = [elementType];
for (const [_typeName, typeDefinition] of this.generatedTypes) {
allTypes.push(typeDefinition);
}
allTypes.push(`type ${this.rootTypeName} = ${elementTypeName}[];`);
return allTypes.join('\n\n');
}
else {
const properties = this.extractProperties(data);
const rootType = this.generateTypeDefinition(properties, this.rootTypeName);
const allTypes = [rootType];
for (const [_typeName, typeDefinition] of this.generatedTypes) {
allTypes.push(typeDefinition);
}
return allTypes.join('\n\n');
}
}
/**
* 新しいオプションでTypeGenインスタンスを生成します
*/
withOptions(options) {
return new TypeGen({
rootTypeName: options.rootTypeName ?? this.rootTypeName,
indentSize: options.indentSize ?? this.indentSize,
arrayElementTypeName: options.arrayElementTypeName ?? this.arrayElementTypeName
});
}
/**
* 再帰的に型情報を抽出(配列にも対応)
*/
extractDeepProperties(obj) {
if (Array.isArray(obj)) {
if (obj.length === 0)
return {};
// 配列の場合は共通プロパティを抽出
const commonProps = this.extractCommonProperties(obj);
const result = {};
for (const prop of commonProps) {
if (typeof obj[0][prop.name] === 'object' && obj[0][prop.name] !== null && !Array.isArray(obj[0][prop.name])) {
// ネストしたオブジェクトの場合、全ての要素から該当プロパティを抽出して再帰処理
const nestedObjects = obj.map(item => item[prop.name]).filter(item => item !== undefined);
result[prop.name] = {
type: prop.type,
optional: prop.optional,
children: this.extractDeepProperties(nestedObjects)
};
}
else {
result[prop.name] = {
type: prop.type,
optional: prop.optional
};
}
}
return result;
}
if (typeof obj !== 'object' || obj === null)
return {};
const result = {};
for (const [key, value] of Object.entries(obj)) {
const type = this.getPropertyType(value, key);
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
result[key] = {
type,
children: this.extractDeepProperties(value)
};
}
else {
result[key] = {
type,
};
}
}
return result;
}
/**
* 再帰的な型アサーション(配列にも対応)
*/
assertDeep(data, expected, path = '') {
const actual = this.extractDeepProperties(data);
for (const key of Object.keys(expected)) {
if (!(key in actual)) {
throw new Error(`Missing property at ${path}${key}`);
}
const exp = expected[key];
const act = actual[key];
// optionalのデフォルト値はfalse
const expectedOptional = exp.optional ?? false;
const actualOptional = act.optional ?? false;
if (exp.type !== act.type || expectedOptional !== actualOptional) {
throw new Error(`Type mismatch at ${path}${key}: expected { type: ${exp.type}, optional: ${expectedOptional} }, got { type: ${act.type}, optional: ${actualOptional} }`);
}
if (exp.children) {
// 配列の場合は、ネストしたオブジェクトも配列として渡す
const nestedData = Array.isArray(data)
? data.map(item => item[key]).filter(item => item !== undefined)
: data[key];
this.assertDeep(nestedData, exp.children, `${path}${key}.`);
}
}
}
}
/**
* データからTypeScriptの型定義を生成するためのユーティリティ
*
* このモジュールは、データの構造から適切なTypeScript型定義を生成します。
* 入力データの形式に応じて、単一オブジェクト用の型定義または配列用の型定義を生成します。
*
* 特徴:
* - 単一オブジェクトから型定義を生成
* - 配列から共通プロパティを抽出して型定義を生成
* - カスタマイズ可能な型名とインデント
* - プロパティ名に基づく型名の自動生成
*
* 型の判定ルール:
* - 配列の場合:
* - 空配列 → any[]
* - 要素がすべて同じ型 → その型の配列(例: number[])
* - 要素の型が混在 → any[]
* - オブジェクトの場合:
* - ネストしたオブジェクト → プロパティ名から生成された型名(例: user → UserType)
* - null/undefined → any型
*
* @example
* // 単一オブジェクトの場合
* const data = {
* id: 1,
* name: "John Doe",
* email: "john@example.com"
* };
* // 生成される型:
* type RootType = {
* id: number;
* name: string;
* email: string;
* }
*
* @example
* // 配列の場合
* const data = [
* {
* id: 1,
* name: "John Doe",
* email: "john@example.com"
* },
* {
* id: 2,
* name: "Jane Smith",
* phone: "123-456-7890"
* }
* ];
* // 生成される型:
* type ContentType = {
* id: number;
* name: string;
* email?: string;
* phone?: string;
* }
* type RootType = ContentType[];
*
* @example
* // 配列の型混在とネストしたオブジェクト
* const data = {
* numbers: [1, 2, 3], // number[]
* strings: ["a", "b", "c"], // string[]
* mixed: [1, "a", true], // any[]
* empty: [], // any[]
* user: { // UserType
* profile: { // ProfileType
* name: "John"
* }
* }
* };
* // 生成される型:
* type ProfileType = {
* name: string;
* }
* type UserType = {
* profile: ProfileType;
* }
* type RootType = {
* numbers: number[];
* strings: string[];
* mixed: any[];
* empty: any[];
* user: UserType;
* }
*
* @note
* - 配列の場合は、すべてのプロパティをオプショナル(?)として扱います
* - 単一オブジェクトの場合は、すべてのプロパティを必須として扱います
* - ネストしたオブジェクトの型名は、プロパティ名から自動生成されます(user → UserType)
* - 配列要素の型は ContentType として生成されます
*/
export { TypeGen };