Skip to content

Commit 2135234

Browse files
committed
feat: enhance getTypeFromValue to support data type extraction and improve type inference
1 parent d3121d3 commit 2135234

4 files changed

Lines changed: 84 additions & 26 deletions

File tree

src/extraction/getTypeFromValue.ts

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import ts from "typescript";
2-
import {createCompilerHost} from "../utils";
3-
import {LiteralValue} from "@code0-tech/sagittarius-graphql-types";
2+
import {createCompilerHost, getSharedTypeDeclarations} from "../utils";
3+
import {DataType, LiteralValue} from "@code0-tech/sagittarius-graphql-types";
44

55
/**
66
* Uses the TypeScript compiler to generate a precise type string from any runtime value.
77
*/
88
export const getTypeFromValue = (
9-
value?: LiteralValue | null
9+
value?: LiteralValue | null,
10+
dataTypes?: DataType[]
1011
): string => {
1112
// 1. Serialize value to a JSON string for embedding in source code.
1213
const literal = JSON.stringify(value?.value);
1314

15+
if (!literal) return "any"
16+
1417
// 2. Wrap value in virtual source code.
15-
const sourceCode = `const tempValue = ${literal ?? "any"};`;
18+
const sourceCode = `
19+
${getSharedTypeDeclarations(dataTypes, "unknown")}
20+
const tempValue = ${literal ?? "any"};
21+
`;
1622
const fileName = "index.ts";
1723
const host = createCompilerHost(fileName, sourceCode);
1824
const sourceFile = host.getSourceFile(fileName)!;
@@ -25,16 +31,64 @@ export const getTypeFromValue = (
2531
const visit = (node: ts.Node) => {
2632
if (ts.isVariableDeclaration(node) && node.name.getText() === "tempValue") {
2733
const type = checker.getTypeAtLocation(node);
28-
inferredType = checker.typeToString(
29-
type,
30-
node,
31-
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseFullyQualifiedType
32-
);
34+
const allAliases = checker.getSymbolsInScope(node, ts.SymbolFlags.TypeAlias);
35+
36+
const resolve = (t: ts.Type): string => {
37+
if (t.isUnion()) {
38+
const parts = t.types.map(resolve);
39+
return Array.from(new Set(parts)).sort().join(" | ");
40+
}
41+
42+
// 2. Array-Handling
43+
if (checker.isArrayType(t)) {
44+
const typeArgs = (t as any).typeArguments || [];
45+
const inner = typeArgs.length > 0 ? resolve(typeArgs[0]) : "any";
46+
return typeArgs[0]?.isUnion() ? `(${inner})[]` : `${inner}[]`;
47+
}
48+
49+
if ((t.getFlags() & ts.TypeFlags.Object) || t.isClassOrInterface()) {
50+
const props = checker.getPropertiesOfType(t);
51+
if (props.length > 0) {
52+
const fields = props.map(p => {
53+
const pType = checker.getTypeOfSymbolAtLocation(p, node);
54+
return `${p.getName()}: ${resolve(pType)}`;
55+
});
56+
return `{ ${fields.join(", ")} }`;
57+
}
58+
}
59+
60+
// 4. Alias-Suche für Blätter (Zahlen, Strings, etc.)
61+
const matches = allAliases.filter(s =>
62+
checker.isTypeAssignableTo(t, checker.getDeclaredTypeOfSymbol(s))
63+
);
64+
65+
if (matches.length > 0) {
66+
const bestMatch = matches.sort((a, b) => {
67+
const typeA = checker.getDeclaredTypeOfSymbol(a);
68+
const typeB = checker.getDeclaredTypeOfSymbol(b);
69+
70+
const aSubB = checker.isTypeAssignableTo(typeA, typeB);
71+
const bSubA = checker.isTypeAssignableTo(typeB, typeA);
72+
73+
if (aSubB && !bSubA) return -1;
74+
if (bSubA && !aSubB) return 1;
75+
76+
return a.getName().length - b.getName().length || a.getName().localeCompare(b.getName());
77+
})[0];
78+
79+
return bestMatch.getName();
80+
}
81+
82+
return checker.typeToString(t, node);
83+
};
84+
85+
inferredType = resolve(type);
3386
}
3487
ts.forEachChild(node, visit);
3588
};
3689

3790
visit(sourceFile);
3891

3992
return inferredType;
40-
};
93+
};
94+

src/utils.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ export const MINIMAL_LIB = `
4545
interface NewableFunction extends Function {}
4646
interface IArguments { }
4747
interface RegExp { }
48-
type Record<K extends keyof any, T> = { [P in K]: T; };
49-
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
5048
`;
5149

5250
/**
@@ -78,7 +76,7 @@ export const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = {
7876
/**
7977
* Extracts and returns common type and generic declarations from DATA_TYPES.
8078
*/
81-
export function getSharedTypeDeclarations(dataTypes?: DataType[], genericType: string = "any"): string {
79+
export function getSharedTypeDeclarations(dataTypes?: DataType[], genericType: string = "any", useGenericDeclarations: boolean = true): string {
8280
const genericDeclarations = Array.from(new Set(dataTypes?.flatMap(dt => dt.genericKeys || [])))
8381
.map(g => `type ${g} = ${genericType};`)
8482
.join("\n");
@@ -87,7 +85,7 @@ export function getSharedTypeDeclarations(dataTypes?: DataType[], genericType: s
8785
`type ${dt.identifier}${(dt.genericKeys?.length ?? 0) > 0 ? `<${dt.genericKeys?.join(",")}>` : ""} = ${dt.type};`
8886
).join("\n");
8987

90-
return `${genericDeclarations}\n${typeAliasDeclarations}`;
88+
return `${useGenericDeclarations ? genericDeclarations : ""}\n${typeAliasDeclarations}`;
9189
}
9290

9391
/**

test/typeFromValue.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { describe, it, expect } from 'vitest';
22
import { getTypeFromValue } from '../src/extraction/getTypeFromValue';
3+
import { DATA_TYPES } from './data';
34

45
describe('getTypeFromValue', () => {
56

67
it('should infer exact string literals', () => {
7-
expect(getTypeFromValue({value: "POST"})).toBe('"POST"');
8-
expect(getTypeFromValue({value: "active"})).toBe('"active"');
8+
expect(getTypeFromValue({value: "POST"}, DATA_TYPES)).toBe('HTTP_METHOD');
9+
expect(getTypeFromValue({value: "active"}, DATA_TYPES)).toBe('TEXT');
910
});
1011

1112
it('should infer exact number and boolean literals', () => {
12-
expect(getTypeFromValue({value: 200})).toBe('200');
13-
expect(getTypeFromValue({value: false})).toBe('false');
13+
expect(getTypeFromValue({value: 200}, DATA_TYPES)).toBe('NUMBER');
14+
expect(getTypeFromValue({value: false}, DATA_TYPES)).toBe('BOOLEAN');
1415
});
1516

1617
it('should infer complex object structures', () => {
@@ -20,24 +21,24 @@ describe('getTypeFromValue', () => {
2021
};
2122

2223

23-
const result = getTypeFromValue({value: user});
24+
const result = getTypeFromValue({value: user}, DATA_TYPES);
2425

25-
expect(result).toMatch(/id:\s*number/);
26-
expect(result).toMatch(/name:\s*string/);
27-
expect(result).toMatch(/age:\s*number/);
26+
expect(result).toMatch(/id:\s*NUMBER/);
27+
expect(result).toMatch(/name:\s*TEXT/);
28+
expect(result).toMatch(/age:\s*NUMBER/);
2829
});
2930

3031
it('should infer arrays as unions of literals', () => {
3132
const list = ["A", "B"];
32-
const result = getTypeFromValue({value: list});
33+
const result = getTypeFromValue({value: list}, DATA_TYPES);
3334

34-
expect(result).toBe("string[]");
35+
expect(result).toBe("TEXT[]");
3536
});
3637

3738
it('should handle nested arrays', () => {
3839
const nested = [1, [2, 3]];
39-
const result = getTypeFromValue({value: nested});
40-
expect(result).toBe('(number | number[])[]');
40+
const result = getTypeFromValue({value: nested}, DATA_TYPES);
41+
expect(result).toBe('(NUMBER | NUMBER[])[]');
4142
});
4243

4344
it('should handle null and empty structures', () => {

test/valueFromType.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ describe('getValueFromType', () => {
4747
expect(result.value).toBe('GET');
4848
});
4949

50+
it('sollte das erste Element einer String-Union nehmen 2', () => {
51+
const result = getValueFromType('HTTP_METHOD', DATA_TYPES);
52+
expect(result.value).toBe('GET');
53+
});
54+
5055
it('sollte bei einer Union aus verschiedenen Typen den ersten Typ wählen', () => {
5156
const result = getValueFromType('number | string', DATA_TYPES);
5257

0 commit comments

Comments
 (0)