Skip to content

Commit

Permalink
Conditional type support (#105)
Browse files Browse the repository at this point in the history
* Add build and test task

* Implement crude conditional type node parser

* Fix linter warning

* Add some tests for conditional type

* Only unwrap types internally when needed

* Update schema URLs to draft-07

* Use already existing derefType

* Remove accidentally commited tasks.json

* Split types "any" and "unknown"

This is needed because for isAssignableTo checks these types works differently

* Add NeverTypeFormatter in case this type is exposed

* Add new classes to index

* Move isAssignable check to utility function

* Better variable names

* Fix stack overflow in isAssignableTo for circular dependencies

* Simplify condition type node parser

* Add test for Omit

* Simplified conditional type node parser even more

* Support enums in conditionals types

* Combine unions

* Deref type

* More tests

* Improve type narrowing so definition types are kept

* Remove accidentally commited comment

* Only narrow down result type when type parameter matching the check type

* Ignore annotations from standard typescript types
  • Loading branch information
domoritz authored Jun 23, 2019
1 parent d3c83ac commit a03ac3d
Show file tree
Hide file tree
Showing 40 changed files with 1,424 additions and 7 deletions.
4 changes: 4 additions & 0 deletions factory/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { EnumTypeFormatter } from "../src/TypeFormatter/EnumTypeFormatter";
import { IntersectionTypeFormatter } from "../src/TypeFormatter/IntersectionTypeFormatter";
import { LiteralTypeFormatter } from "../src/TypeFormatter/LiteralTypeFormatter";
import { LiteralUnionTypeFormatter } from "../src/TypeFormatter/LiteralUnionTypeFormatter";
import { NeverTypeFormatter } from "../src/TypeFormatter/NeverTypeFormatter";
import { NullTypeFormatter } from "../src/TypeFormatter/NullTypeFormatter";
import { NumberTypeFormatter } from "../src/TypeFormatter/NumberTypeFormatter";
import { ObjectTypeFormatter } from "../src/TypeFormatter/ObjectTypeFormatter";
Expand All @@ -23,6 +24,7 @@ import { StringTypeFormatter } from "../src/TypeFormatter/StringTypeFormatter";
import { TupleTypeFormatter } from "../src/TypeFormatter/TupleTypeFormatter";
import { UndefinedTypeFormatter } from "../src/TypeFormatter/UndefinedTypeFormatter";
import { UnionTypeFormatter } from "../src/TypeFormatter/UnionTypeFormatter";
import { UnknownTypeFormatter } from "../src/TypeFormatter/UnknownTypeFormatter";



Expand All @@ -39,7 +41,9 @@ export function createFormatter(config: Config): TypeFormatter {
.addTypeFormatter(new NullTypeFormatter())

.addTypeFormatter(new AnyTypeFormatter())
.addTypeFormatter(new NeverTypeFormatter())
.addTypeFormatter(new UndefinedTypeFormatter())
.addTypeFormatter(new UnknownTypeFormatter())

.addTypeFormatter(new LiteralTypeFormatter())
.addTypeFormatter(new EnumTypeFormatter())
Expand Down
7 changes: 6 additions & 1 deletion factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { ArrayNodeParser } from "../src/NodeParser/ArrayNodeParser";
import { BooleanLiteralNodeParser } from "../src/NodeParser/BooleanLiteralNodeParser";
import { BooleanTypeNodeParser } from "../src/NodeParser/BooleanTypeNodeParser";
import { CallExpressionParser } from "../src/NodeParser/CallExpressionParser";
import { ConditionalTypeNodeParser } from "../src/NodeParser/ConditionalTypeNodeParser";
import { EnumNodeParser } from "../src/NodeParser/EnumNodeParser";
import { ExpressionWithTypeArgumentsNodeParser } from "../src/NodeParser/ExpressionWithTypeArgumentsNodeParser";
import { IndexedAccessTypeNodeParser } from "../src/NodeParser/IndexedAccessTypeNodeParser";
import { InterfaceAndClassNodeParser } from "../src/NodeParser/InterfaceAndClassNodeParser";
import { IntersectionNodeParser } from "../src/NodeParser/IntersectionNodeParser";
import { LiteralNodeParser } from "../src/NodeParser/LiteralNodeParser";
import { MappedTypeNodeParser } from "../src/NodeParser/MappedTypeNodeParser";
import { NeverTypeNodeParser } from "../src/NodeParser/NeverTypeNodeParser";
import { NullLiteralNodeParser } from "../src/NodeParser/NullLiteralNodeParser";
import { NumberLiteralNodeParser } from "../src/NodeParser/NumberLiteralNodeParser";
import { NumberTypeNodeParser } from "../src/NodeParser/NumberTypeNodeParser";
Expand All @@ -37,11 +39,11 @@ import { TypeOperatorNodeParser } from "../src/NodeParser/TypeOperatorNodeParser
import { TypeReferenceNodeParser } from "../src/NodeParser/TypeReferenceNodeParser";
import { UndefinedTypeNodeParser } from "../src/NodeParser/UndefinedTypeNodeParser";
import { UnionNodeParser } from "../src/NodeParser/UnionNodeParser";
import { UnknownTypeNodeParser } from "../src/NodeParser/UnknownTypeNodeParser";
import { SubNodeParser } from "../src/SubNodeParser";
import { TopRefNodeParser } from "../src/TopRefNodeParser";
import { FunctionNodeParser } from "./../src/NodeParser/FunctionNodeParser";


export function createParser(program: ts.Program, config: Config): NodeParser {
const typeChecker = program.getTypeChecker();
const chainNodeParser = new ChainNodeParser(typeChecker, []);
Expand Down Expand Up @@ -72,7 +74,9 @@ export function createParser(program: ts.Program, config: Config): NodeParser {
.addNodeParser(new NumberTypeNodeParser())
.addNodeParser(new BooleanTypeNodeParser())
.addNodeParser(new AnyTypeNodeParser())
.addNodeParser(new UnknownTypeNodeParser())
.addNodeParser(new UndefinedTypeNodeParser())
.addNodeParser(new NeverTypeNodeParser())
.addNodeParser(new ObjectTypeNodeParser())

.addNodeParser(new StringLiteralNodeParser())
Expand All @@ -92,6 +96,7 @@ export function createParser(program: ts.Program, config: Config): NodeParser {
.addNodeParser(new IndexedAccessTypeNodeParser(chainNodeParser))
.addNodeParser(new TypeofNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new MappedTypeNodeParser(chainNodeParser))
.addNodeParser(new ConditionalTypeNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new TypeOperatorNodeParser(chainNodeParser))

.addNodeParser(new UnionNodeParser(typeChecker, chainNodeParser))
Expand Down
5 changes: 5 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export * from "./src/SubTypeFormatter";
export * from "./src/ChainTypeFormatter";
export * from "./src/CircularReferenceTypeFormatter";
export * from "./src/TypeFormatter/AnyTypeFormatter";
export * from "./src/TypeFormatter/UnknownTypeFormatter";
export * from "./src/TypeFormatter/NullTypeFormatter";
export * from "./src/TypeFormatter/UndefinedTypeFormatter";
export * from "./src/TypeFormatter/NeverTypeFormatter";
export * from "./src/TypeFormatter/BooleanTypeFormatter";
export * from "./src/TypeFormatter/NumberTypeFormatter";
export * from "./src/TypeFormatter/StringTypeFormatter";
Expand All @@ -71,9 +73,11 @@ export * from "./src/ExposeNodeParser";
export * from "./src/TopRefNodeParser";
export * from "./src/CircularReferenceNodeParser";
export * from "./src/NodeParser/AnyTypeNodeParser";
export * from "./src/NodeParser/UnknownTypeNodeParser";
export * from "./src/NodeParser/LiteralNodeParser";
export * from "./src/NodeParser/NullLiteralNodeParser";
export * from "./src/NodeParser/UndefinedTypeNodeParser";
export * from "./src/NodeParser/NeverTypeNodeParser";
export * from "./src/NodeParser/NumberLiteralNodeParser";
export * from "./src/NodeParser/StringLiteralNodeParser";
export * from "./src/NodeParser/BooleanLiteralNodeParser";
Expand All @@ -93,6 +97,7 @@ export * from "./src/NodeParser/UnionNodeParser";
export * from "./src/NodeParser/TupleNodeParser";
export * from "./src/NodeParser/AnnotatedNodeParser";
export * from "./src/NodeParser/CallExpressionParser";
export * from "./src/NodeParser/ConditionalTypeNodeParser";
export * from "./src/NodeParser/PrefixUnaryExpressionNodeParser";

export * from "./src/SchemaGenerator";
Expand Down
3 changes: 3 additions & 0 deletions src/NodeParser/AnnotatedNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export class AnnotatedNodeParser implements SubNodeParser {

public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType {
const baseType = this.childNodeParser.createType(node, context, reference);
if (node.getSourceFile().fileName.match(/[\/\\]typescript[\/\\]lib[\/\\]lib\.[^/\\]+\.d\.ts$/i)) {
return baseType;
}
const annotatedNode = this.getAnnotatedNode(node);
const annotations = this.annotationsReader.getAnnotations(annotatedNode);
const nullable = this.annotationsReader instanceof ExtendedAnnotationsReader ?
Expand Down
2 changes: 1 addition & 1 deletion src/NodeParser/AnyTypeNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BaseType } from "../Type/BaseType";

export class AnyTypeNodeParser implements SubNodeParser {
public supportsNode(node: ts.KeywordTypeNode): boolean {
return node.kind === ts.SyntaxKind.AnyKeyword || node.kind === ts.SyntaxKind.UnknownKeyword;
return node.kind === ts.SyntaxKind.AnyKeyword;
}
public createType(node: ts.KeywordTypeNode, context: Context): BaseType {
return new AnyType();
Expand Down
50 changes: 50 additions & 0 deletions src/NodeParser/ConditionalTypeNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as ts from "typescript";
import { Context, NodeParser } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { isAssignableTo } from "../Utils/isAssignableTo";
import { narrowType } from "../Utils/narrowType";

export class ConditionalTypeNodeParser implements SubNodeParser {
public constructor(
private typeChecker: ts.TypeChecker,
private childNodeParser: NodeParser,
) {}

public supportsNode(node: ts.ConditionalTypeNode): boolean {
return node.kind === ts.SyntaxKind.ConditionalType;
}

public createType(node: ts.ConditionalTypeNode, context: Context): BaseType {
const checkType = this.childNodeParser.createType(node.checkType, context);
const extendsType = this.childNodeParser.createType(node.extendsType, context);
const result = isAssignableTo(extendsType, checkType);
const tsResultType = result ? node.trueType : node.falseType;
const resultType = this.childNodeParser.createType(tsResultType, context);

// If result type is the same type parameter as the check type then narrow down the result type
const checkTypeParameterName = this.getTypeParameterName(node.checkType);
const resultTypeParameterName = this.getTypeParameterName(tsResultType);
if (resultTypeParameterName != null && resultTypeParameterName === checkTypeParameterName) {
return narrowType(resultType, type => isAssignableTo(extendsType, type) === result);
}

return resultType;
}

/**
* Returns the type parameter name of the given type node if any.
*
* @param node - The type node for which to return the type parameter name.
* @return The type parameter name or null if specified type node is not a type parameter.
*/
private getTypeParameterName(node: ts.TypeNode): string | null {
if (ts.isTypeReferenceNode(node)) {
const typeSymbol = this.typeChecker.getSymbolAtLocation(node.typeName)!;
if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) {
return typeSymbol.name;
}
}
return null;
}
}
14 changes: 14 additions & 0 deletions src/NodeParser/NeverTypeNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as ts from "typescript";
import { Context } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { NeverType } from "../Type/NeverType";

export class NeverTypeNodeParser implements SubNodeParser {
public supportsNode(node: ts.KeywordTypeNode): boolean {
return node.kind === ts.SyntaxKind.NeverKeyword;
}
public createType(node: ts.KeywordTypeNode, context: Context): BaseType {
return new NeverType();
}
}
14 changes: 14 additions & 0 deletions src/NodeParser/UnknownTypeNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as ts from "typescript";
import { Context } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { UnknownType } from "../Type/UnknownType";

export class UnknownTypeNodeParser implements SubNodeParser {
public supportsNode(node: ts.KeywordTypeNode): boolean {
return node.kind === ts.SyntaxKind.UnknownKeyword;
}
public createType(node: ts.KeywordTypeNode, context: Context): BaseType {
return new UnknownType();
}
}
9 changes: 9 additions & 0 deletions src/Type/EnumType.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { BaseType } from "./BaseType";
import { LiteralType } from "./LiteralType";
import { NullType } from "./NullType";

export type EnumValue = string|boolean|number|null;

export class EnumType extends BaseType {
private types: BaseType[];

public constructor(
private id: string,
private values: EnumValue[],
) {
super();
this.types = values.map(value => value == null ? new NullType() : new LiteralType(value));
}

public getId(): string {
Expand All @@ -17,4 +22,8 @@ export class EnumType extends BaseType {
public getValues(): EnumValue[] {
return this.values;
}

public getTypes(): BaseType[] {
return this.types;
}
}
7 changes: 7 additions & 0 deletions src/Type/NeverType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BaseType } from "./BaseType";

export class NeverType extends BaseType {
public getId(): string {
return "never";
}
}
7 changes: 7 additions & 0 deletions src/Type/UnknownType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BaseType } from "./BaseType";

export class UnknownType extends BaseType {
public getId(): string {
return "unknown";
}
}
16 changes: 16 additions & 0 deletions src/TypeFormatter/NeverTypeFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Definition } from "../Schema/Definition";
import { SubTypeFormatter } from "../SubTypeFormatter";
import { BaseType } from "../Type/BaseType";
import { NeverType } from "../Type/NeverType";

export class NeverTypeFormatter implements SubTypeFormatter {
public supportsType(type: NeverType): boolean {
return type instanceof NeverType;
}
public getDefinition(type: NeverType): Definition {
return {not: {}};
}
public getChildren(type: NeverType): BaseType[] {
return [];
}
}
3 changes: 2 additions & 1 deletion src/TypeFormatter/ObjectTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { UndefinedType } from "../Type/UndefinedType";
import { UnionType } from "../Type/UnionType";
import { TypeFormatter } from "../TypeFormatter";
import { getAllOfDefinitionReducer } from "../Utils/allOfDefinition";
import { derefType } from "../Utils/derefType";
import { StringMap } from "../Utils/StringMap";

export class ObjectTypeFormatter implements SubTypeFormatter {
Expand Down Expand Up @@ -75,7 +76,7 @@ export class ObjectTypeFormatter implements SubTypeFormatter {
}

private prepareObjectProperty(property: ObjectProperty): ObjectProperty {
const propType = property.getType();
const propType = derefType(property.getType());
if (propType instanceof UndefinedType) {
return new ObjectProperty(property.getName(), new UndefinedType(), false);
} else if (!(propType instanceof UnionType)) {
Expand Down
16 changes: 16 additions & 0 deletions src/TypeFormatter/UnknownTypeFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Definition } from "../Schema/Definition";
import { SubTypeFormatter } from "../SubTypeFormatter";
import { BaseType } from "../Type/BaseType";
import { UnknownType } from "../Type/UnknownType";

export class UnknownTypeFormatter implements SubTypeFormatter {
public supportsType(type: UnknownType): boolean {
return type instanceof UnknownType;
}
public getDefinition(type: UnknownType): Definition {
return {};
}
public getChildren(type: UnknownType): BaseType[] {
return [];
}
}
Loading

0 comments on commit a03ac3d

Please sign in to comment.