Skip to content

Commit c8ea3ed

Browse files
fix: support for generic functions (#2159)
Co-authored-by: Dominik Moritz <domoritz@gmail.com>
1 parent 6ad76cf commit c8ea3ed

File tree

13 files changed

+263
-13
lines changed

13 files changed

+263
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"vega": "^5.31.0",
9494
"vega-lite": "^5.23.0"
9595
},
96+
"packageManager": "npm@10.8.2",
9697
"engines": {
9798
"node": ">=18.0.0"
9899
}

src/ChainNodeParser.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class ChainNodeParser implements SubNodeParser, MutableParser {
3131
}
3232
const contextCacheKey = context.getCacheKey();
3333
let type = typeCache.get(contextCacheKey);
34+
3435
if (!type) {
3536
try {
3637
type = this.getNodeParser(node).createType(node, context, reference);
@@ -41,6 +42,11 @@ export class ChainNodeParser implements SubNodeParser, MutableParser {
4142
typeCache.set(contextCacheKey, type);
4243
}
4344
}
45+
46+
if (!type) {
47+
throw new UnknownNodeError(node);
48+
}
49+
4450
return type;
4551
}
4652

src/NodeParser/CallExpressionParser.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { BaseType } from "../Type/BaseType.js";
77
import { UnionType } from "../Type/UnionType.js";
88
import { LiteralType } from "../Type/LiteralType.js";
99
import { SymbolType } from "../Type/SymbolType.js";
10+
import { UnknownNodeError } from "../Error/Errors.js";
1011

1112
export class CallExpressionParser implements SubNodeParser {
1213
public constructor(
@@ -33,9 +34,21 @@ export class CallExpressionParser implements SubNodeParser {
3334
}
3435

3536
const symbol = type.symbol || type.aliasSymbol;
36-
const decl = symbol.valueDeclaration || symbol.declarations![0];
37-
const subContext = this.createSubContext(node, context);
38-
return this.childNodeParser.createType(decl, subContext);
37+
38+
// For funtions like <T>(type: T) => T, there won't be any reference to the original
39+
// type. Using type checker to infer the actual return type without mapping the whole
40+
// function and back referencing its generic type based on parameter index is a better
41+
// approach.
42+
const decl =
43+
this.typeChecker.typeToTypeNode(type, node, ts.NodeBuilderFlags.IgnoreErrors) ||
44+
symbol.valueDeclaration ||
45+
symbol.declarations?.[0];
46+
47+
if (!decl) {
48+
throw new UnknownNodeError(node);
49+
}
50+
51+
return this.childNodeParser.createType(decl, this.createSubContext(node, context));
3952
}
4053

4154
protected createSubContext(node: ts.CallExpression, parentContext: Context): Context {

src/NodeParser/IndexedAccessTypeNodeParser.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ts from "typescript";
2+
import { LogicError } from "../Error/Errors.js";
23
import type { Context, NodeParser } from "../NodeParser.js";
34
import type { SubNodeParser } from "../SubNodeParser.js";
45
import type { BaseType } from "../Type/BaseType.js";
@@ -9,9 +10,9 @@ import { ReferenceType } from "../Type/ReferenceType.js";
910
import { StringType } from "../Type/StringType.js";
1011
import { TupleType } from "../Type/TupleType.js";
1112
import { UnionType } from "../Type/UnionType.js";
13+
import { isErroredUnknownType } from "../Type/UnknownType.js";
1214
import { derefType } from "../Utils/derefType.js";
1315
import { getTypeByKey } from "../Utils/typeKeys.js";
14-
import { LogicError } from "../Error/Errors.js";
1516

1617
export class IndexedAccessTypeNodeParser implements SubNodeParser {
1718
public constructor(
@@ -49,7 +50,7 @@ export class IndexedAccessTypeNodeParser implements SubNodeParser {
4950
const indexType = derefType(this.childNodeParser.createType(node.indexType, context));
5051
const indexedType = this.createIndexedType(node.objectType, context, indexType);
5152

52-
if (indexedType) {
53+
if (indexedType && !isErroredUnknownType(indexedType)) {
5354
return indexedType;
5455
}
5556

src/NodeParser/TypeReferenceNodeParser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AnyType } from "../Type/AnyType.js";
66
import { ArrayType } from "../Type/ArrayType.js";
77
import type { BaseType } from "../Type/BaseType.js";
88
import { StringType } from "../Type/StringType.js";
9+
import { UnknownType } from "../Type/UnknownType.js";
910
import { symbolAtNode } from "../Utils/symbolAtNode.js";
1011

1112
const invalidTypes: Record<number, boolean> = {
@@ -45,7 +46,7 @@ export class TypeReferenceNodeParser implements SubNodeParser {
4546
}
4647

4748
if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) {
48-
return context.getArgument(typeSymbol.name);
49+
return context.getArgument(typeSymbol.name) ?? new UnknownType(true);
4950
}
5051

5152
// Wraps promise type to avoid resolving to a empty Object type.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import ts from "typescript";
2-
import type { Context } from "../NodeParser.js";
32
import type { SubNodeParser } from "../SubNodeParser.js";
43
import type { BaseType } from "../Type/BaseType.js";
54
import { UnknownType } from "../Type/UnknownType.js";
@@ -8,7 +7,8 @@ export class UnknownTypeNodeParser implements SubNodeParser {
87
public supportsNode(node: ts.KeywordTypeNode): boolean {
98
return node.kind === ts.SyntaxKind.UnknownKeyword;
109
}
11-
public createType(node: ts.KeywordTypeNode, context: Context): BaseType {
12-
return new UnknownType();
10+
11+
public createType(): BaseType {
12+
return new UnknownType(false);
1313
}
1414
}

src/Type/UnknownType.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import { BaseType } from "./BaseType.js";
22

33
export class UnknownType extends BaseType {
4-
constructor() {
4+
constructor(
5+
/**
6+
* If the source for this UnknownType was from a failed operation than to an actual `unknown` type present in the source code.
7+
*/
8+
readonly erroredSource: boolean,
9+
) {
510
super();
611
}
12+
713
public getId(): string {
814
return "unknown";
915
}
1016
}
17+
18+
/**
19+
* Checks for an UnknownType with an errored source.
20+
*/
21+
export function isErroredUnknownType(type: BaseType): type is UnknownType {
22+
return type instanceof UnknownType && type.erroredSource;
23+
}

src/TypeFormatter/UnknownTypeFormatter.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ export class UnknownTypeFormatter implements SubTypeFormatter {
77
public supportsType(type: BaseType): boolean {
88
return type instanceof UnknownType;
99
}
10+
1011
public getDefinition(type: UnknownType): Definition {
12+
if (type.erroredSource) {
13+
return { description: "Failed to correctly infer type" };
14+
}
15+
1116
return {};
1217
}
13-
public getChildren(type: UnknownType): BaseType[] {
18+
19+
public getChildren(): BaseType[] {
1420
return [];
1521
}
1622
}

test/valid-data-other.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ describe("valid-data-other", () => {
8484
it("array-function-generics", assertValidSchema("array-function-generics", "*"));
8585
it("array-max-items-optional", assertValidSchema("array-max-items-optional", "MyType"));
8686
it("shorthand-array", assertValidSchema("shorthand-array", "MyType"));
87+
it("function-generic", assertValidSchema("function-generic", "MyType"));
8788

8889
it(
8990
"object-required",

test/valid-data/array-function-generics/schema.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99
"additionalProperties": false,
1010
"properties": {
1111
"a": {
12-
"items": {},
12+
"items": {
13+
"description": "Failed to correctly infer type"
14+
},
1315
"type": "array"
1416
},
1517
"b": {
16-
"items": {},
18+
"items": {
19+
"description": "Failed to correctly infer type"
20+
},
1721
"type": "array"
1822
}
1923
},

0 commit comments

Comments
 (0)