-
Notifications
You must be signed in to change notification settings - Fork 215
feat: Improved Promise handling to support packages like Prisma #1924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 21 commits
717bb1a
68b6c0b
09ff47a
19e481f
2d36fbf
2517d66
a13545c
c78f38c
8f80837
624736e
7944bc7
697805f
a05e492
9ca48e0
ec698e2
3b74479
a4a497f
2a3e4ce
42d2bd9
e5daa4a
d6ed5a7
6b11f65
8100f43
e804590
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,6 @@ node_modules/ | |
# local config for auto | ||
.env | ||
|
||
# Other package managers | ||
pnpm-lock.yaml | ||
package-lock.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,6 +65,7 @@ | |
"@types/jest": "^29.5.12", | ||
"@types/node": "^20.12.7", | ||
"@types/normalize-path": "^3.0.2", | ||
"@types/ts-expose-internals": "npm:ts-expose-internals@^5.4.5", | ||
"ajv": "^8.12.0", | ||
"ajv-formats": "^3.0.1", | ||
"auto": "^11.1.6", | ||
|
@@ -94,6 +95,5 @@ | |
"debug": "tsx --inspect-brk ts-json-schema-generator.ts", | ||
"run": "tsx ts-json-schema-generator.ts", | ||
"release": "yarn build && auto shipit" | ||
}, | ||
"packageManager": "[email protected]" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import ts from "typescript"; | ||
import { Context, type NodeParser } from "../NodeParser.js"; | ||
import type { SubNodeParser } from "../SubNodeParser.js"; | ||
import { AliasType } from "../Type/AliasType.js"; | ||
import type { BaseType } from "../Type/BaseType.js"; | ||
import { DefinitionType } from "../Type/DefinitionType.js"; | ||
import { getKey } from "../Utils/nodeKey.js"; | ||
|
||
/** | ||
* Needs to be registered before 261, 260, 230, 262 node kinds | ||
*/ | ||
export class PromiseNodeParser implements SubNodeParser { | ||
public constructor( | ||
protected typeChecker: ts.TypeChecker, | ||
protected childNodeParser: NodeParser, | ||
) {} | ||
|
||
public supportsNode(node: ts.Node): boolean { | ||
if ( | ||
// 261 interface PromiseInterface extends Promise<T> | ||
!ts.isInterfaceDeclaration(node) && | ||
// 260 class PromiseClass implements Promise<T> | ||
!ts.isClassDeclaration(node) && | ||
// 230 Promise<T> | ||
!ts.isExpressionWithTypeArguments(node) && | ||
// 262 type PromiseAlias = Promise<T>; | ||
!ts.isTypeAliasDeclaration(node) | ||
) { | ||
return false; | ||
} | ||
|
||
const type = this.typeChecker.getTypeAtLocation(node); | ||
|
||
const awaitedType = this.typeChecker.getAwaitedType(type); | ||
|
||
// ignores non awaitable types | ||
if (!awaitedType) { | ||
return false; | ||
} | ||
|
||
// If the awaited type differs from the original type, the type extends promise | ||
// Awaited<Promise<T>> -> T (Promise<T> !== T) | ||
// Awaited<Y> -> Y (Y === Y) | ||
if (awaitedType === type) { | ||
return false; | ||
} | ||
|
||
// In types like: A<T> = T, type C = A<1>, C has the same type as A<1> and 1, | ||
// the awaitedType is NOT the same reference as the type, so a assignability | ||
// check is needed | ||
return ( | ||
!this.typeChecker.isTypeAssignableTo(type, awaitedType) && | ||
!this.typeChecker.isTypeAssignableTo(awaitedType, type) | ||
); | ||
} | ||
|
||
public createType( | ||
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.ExpressionWithTypeArguments | ts.TypeAliasDeclaration, | ||
context: Context, | ||
): BaseType { | ||
const type = this.typeChecker.getTypeAtLocation(node); | ||
const awaitedType = this.typeChecker.getAwaitedType(type)!; // supportsNode ensures this | ||
const awaitedNode = this.typeChecker.typeToTypeNode(awaitedType, undefined, ts.NodeBuilderFlags.IgnoreErrors); | ||
|
||
if (!awaitedNode) { | ||
throw new Error( | ||
`Could not find awaited node for type ${node.pos === -1 ? "<unresolved>" : node.getText()}`, | ||
); | ||
} | ||
|
||
const baseNode = this.childNodeParser.createType(awaitedNode, new Context(node)); | ||
|
||
const name = this.getNodeName(node); | ||
|
||
// Nodes without name should just be their awaited type | ||
// export class extends Promise<T> {} -> T | ||
// export class A extends Promise<T> {} -> A (ref to T) | ||
if (!name) { | ||
return baseNode; | ||
} | ||
|
||
return new DefinitionType(name, new AliasType(`promise-${getKey(node, context)}`, baseNode)); | ||
} | ||
|
||
private getNodeName( | ||
node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.ExpressionWithTypeArguments | ts.TypeAliasDeclaration, | ||
) { | ||
if (ts.isExpressionWithTypeArguments(node)) { | ||
if (!ts.isHeritageClause(node.parent)) { | ||
throw new Error("Expected ExpressionWithTypeArguments to have a HeritageClause parent"); | ||
} | ||
|
||
return node.parent.parent.name?.getText(); | ||
} | ||
|
||
return node.name?.getText(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
import ts from "typescript"; | ||
|
||
import { Context, NodeParser } from "../NodeParser.js"; | ||
import { Context, type NodeParser } from "../NodeParser.js"; | ||
import type { SubNodeParser } from "../SubNodeParser.js"; | ||
import { AnnotatedType } from "../Type/AnnotatedType.js"; | ||
import { AnyType } from "../Type/AnyType.js"; | ||
|
@@ -31,11 +30,6 @@ export class TypeReferenceNodeParser implements SubNodeParser { | |
// property on the node itself. | ||
(node.typeName as unknown as ts.Type).symbol; | ||
|
||
// Wraps promise type to avoid resolving to a empty Object type. | ||
if (typeSymbol.name === "Promise") { | ||
return this.childNodeParser.createType(node.typeArguments![0], this.createSubContext(node, context)); | ||
} | ||
|
||
if (typeSymbol.flags & ts.SymbolFlags.Alias) { | ||
const aliasedSymbol = this.typeChecker.getAliasedSymbol(typeSymbol); | ||
|
||
|
@@ -53,6 +47,16 @@ export class TypeReferenceNodeParser implements SubNodeParser { | |
return context.getArgument(typeSymbol.name); | ||
} | ||
|
||
// Wraps promise type to avoid resolving to a empty Object type. | ||
if (typeSymbol.name === "Promise" || typeSymbol.name === "PromiseLike") { | ||
// Promise without type resolves to Promise<any> | ||
if (!node.typeArguments || node.typeArguments.length === 0) { | ||
return new AnyType(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move this logic into the PromiseNodeParser? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually not, It might be a skill issue of mine but I couldn't get it to work without this statement. When I remove it, this is the error thrown: ● valid-data-type › promise-extensions
TypeError: Cannot read properties of undefined (reading 'getId')
123 |
124 | // Check for simple type equality
> 125 | if (source.getId() === target.getId()) {
| ^
126 | return true;
127 | }
128 |
at getId (src/Utils/isAssignableTo.ts:125:16)
at src/NodeParser/ConditionalTypeNodeParser.ts:44:77
at predicate (src/Utils/narrowType.ts:59:12)
at ConditionalTypeNodeParser.createType (src/NodeParser/ConditionalTypeNodeParser.ts:44:41)
at ChainNodeParser.createType (src/ChainNodeParser.ts:35:54)
at TypeAliasNodeParser.createType (src/NodeParser/TypeAliasNodeParser.ts:40:43)
at AnnotatedNodeParser.createType (src/NodeParser/AnnotatedNodeParser.ts:34:47)
at ExposeNodeParser.createType (src/ExposeNodeParser.ts:23:45)
at CircularReferenceNodeParser.createType (src/CircularReferenceNodeParser.ts:24:43)
at ChainNodeParser.createType (src/ChainNodeParser.ts:35:54)
at TypeReferenceNodeParser.createType (src/NodeParser/TypeReferenceNodeParser.ts:78:37)
at ChainNodeParser.createType (src/ChainNodeParser.ts:35:54)
at PromiseNodeParser.createType (src/NodeParser/PromiseNodeParser.ts:71:47)
at ChainNodeParser.createType (src/ChainNodeParser.ts:35:54)
at TypeReferenceNodeParser.createType (src/NodeParser/TypeReferenceNodeParser.ts:78:37)
at ChainNodeParser.createType (src/ChainNodeParser.ts:35:54)
at createType (src/NodeParser/UnionNodeParser.ts:22:45)
at Array.map (<anonymous>)
at UnionNodeParser.map [as createType] (src/NodeParser/UnionNodeParser.ts:21:14)
at ChainNodeParser.createType (src/ChainNodeParser.ts:35:54)
at TypeAliasNodeParser.createType (src/NodeParser/TypeAliasNodeParser.ts:40:43)
at AnnotatedNodeParser.createType (src/NodeParser/AnnotatedNodeParser.ts:34:47)
at ExposeNodeParser.createType (src/ExposeNodeParser.ts:23:45)
at CircularReferenceNodeParser.createType (src/CircularReferenceNodeParser.ts:24:43)
at ChainNodeParser.createType (src/ChainNodeParser.ts:35:54)
at TopRefNodeParser.createType (src/TopRefNodeParser.ts:14:47)
at createType (src/SchemaGenerator.ts:30:36)
at Array.map (<anonymous>)
at SchemaGenerator.map [as createSchemaFromNodes] (src/SchemaGenerator.ts:29:37)
at SchemaGenerator.createSchemaFromNodes [as createSchema] (src/SchemaGenerator.ts:25:21)
at Object.createSchema (test/utils.ts:61:34) |
||
} | ||
|
||
return this.childNodeParser.createType(node.typeArguments[0], this.createSubContext(node, context)); | ||
} | ||
|
||
if (typeSymbol.name === "Array" || typeSymbol.name === "ReadonlyArray") { | ||
const type = this.createSubContext(node, context).getArguments()[0]; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
export type A = { a: string; b: number[] }; | ||
|
||
export type PromiseAlias = Promise<A>; | ||
|
||
export class PromiseClass extends Promise<A> {} | ||
|
||
export interface PromiseInterface extends Promise<A> {} | ||
|
||
export type LikeType = PromiseLike<A>; | ||
|
||
export type PromiseOrAlias = Promise<A> | A; | ||
|
||
export type LikeOrType = PromiseLike<A> | A; | ||
|
||
export type AndPromise = Promise<A> & { a: string }; | ||
|
||
export type AndLikePromise = PromiseLike<A> & { a: string }; | ||
|
||
// Should not be present | ||
export default class extends Promise<A> {} | ||
|
||
export class LikeClass implements PromiseLike<A> { | ||
then<TResult1 = A, TResult2 = never>( | ||
onfulfilled?: ((value: A) => TResult1 | PromiseLike<TResult1>) | null | undefined, | ||
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined | ||
): PromiseLike<TResult1 | TResult2> { | ||
return new Promise(() => {}); | ||
} | ||
} | ||
|
||
export abstract class LikeAbstractClass implements PromiseLike<A> { | ||
abstract then<TResult1 = A, TResult2 = never>( | ||
onfulfilled?: ((value: A) => TResult1 | PromiseLike<TResult1>) | null | undefined, | ||
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined | ||
); | ||
} | ||
|
||
export interface LikeInterface extends PromiseLike<A> {} | ||
|
||
// Prisma has a base promise type just like this | ||
export interface WithProperty extends Promise<A> { | ||
[Symbol.toStringTag]: "WithProperty"; | ||
} | ||
|
||
export interface ThenableInterface { | ||
then<TResult1 = A, TResult2 = never>( | ||
onfulfilled?: ((value: A) => TResult1 | PromiseLike<TResult1>) | null | undefined, | ||
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined | ||
): PromiseLike<TResult1 | TResult2>; | ||
} | ||
|
||
export class ThenableClass { | ||
then<TResult1 = A, TResult2 = never>( | ||
onfulfilled?: ((value: A) => TResult1 | PromiseLike<TResult1>) | null | undefined, | ||
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined | ||
): PromiseLike<TResult1 | TResult2> { | ||
return new Promise(() => {}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"definitions": { | ||
"A": { | ||
"additionalProperties": false, | ||
"properties": { | ||
"a": { | ||
"type": "string" | ||
}, | ||
"b": { | ||
"items": { | ||
"type": "number" | ||
}, | ||
"type": "array" | ||
} | ||
}, | ||
"required": [ | ||
"a", | ||
"b" | ||
], | ||
"type": "object" | ||
}, | ||
"AndLikePromise": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"AndPromise": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"LikeAbstractClass": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"LikeClass": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"LikeInterface": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"LikeOrType": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"LikeType": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"PromiseAlias": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"PromiseClass": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"PromiseInterface": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"PromiseOrAlias": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"ThenableClass": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"ThenableInterface": { | ||
"$ref": "#/definitions/A" | ||
}, | ||
"WithProperty": { | ||
"$ref": "#/definitions/A" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27149,6 +27149,8 @@ | |
}, | ||
"SingleDefUnitChannel": { | ||
"enum": [ | ||
"text", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somehow this type started getting out of order and breaking CI: https://github.com/vega/ts-json-schema-generator/actions/runs/9105537200/job/25031273060#step:7:25 I changed its order so tests can pass, I'm also almost sure |
||
"shape", | ||
"x", | ||
"y", | ||
"xOffset", | ||
|
@@ -27173,9 +27175,7 @@ | |
"strokeDash", | ||
"size", | ||
"angle", | ||
"shape", | ||
"key", | ||
"text", | ||
"href", | ||
"url", | ||
"description" | ||
|
Uh oh!
There was an error while loading. Please reload this page.