From 52a893b33b15ea74cac505dc981b78dac2d809de Mon Sep 17 00:00:00 2001 From: Adrian Leonhard Date: Fri, 5 Oct 2018 17:30:41 +0200 Subject: [PATCH] Add support for conditional type. --- src/lib/converter/types/conditional.ts | 58 +++++++++ src/lib/converter/types/index.ts | 1 + .../converter/types/union-or-intersection.ts | 2 +- src/lib/models/types/conditional.ts | 72 +++++++++++ src/lib/models/types/index.ts | 1 + .../serializers/types/conditional.ts | 24 ++++ .../serialization/serializers/types/index.ts | 1 + .../conditional-type/conditional-type.ts | 8 +- .../converter/conditional-type/specs.json | 114 +++++++++++++++--- 9 files changed, 261 insertions(+), 20 deletions(-) create mode 100644 src/lib/converter/types/conditional.ts create mode 100644 src/lib/models/types/conditional.ts create mode 100644 src/lib/serialization/serializers/types/conditional.ts diff --git a/src/lib/converter/types/conditional.ts b/src/lib/converter/types/conditional.ts new file mode 100644 index 000000000..126b2cc43 --- /dev/null +++ b/src/lib/converter/types/conditional.ts @@ -0,0 +1,58 @@ +import * as ts from 'typescript'; + +import { ConditionalType } from '../../models/types'; +import { Component, ConverterTypeComponent, TypeConverter } from '../components'; +import { Context } from '../context'; + +@Component({name: 'type:conditional'}) +export class ConditionalConverter extends ConverterTypeComponent implements TypeConverter { + /** + * Test whether this converter can handle the given TypeScript node. + */ + supportsNode(context: Context, node: ts.ConditionalTypeNode): boolean { + return node.kind === ts.SyntaxKind.ConditionalType; + } + + /** + * Test whether this converter can handle the given TypeScript type. + */ + supportsType(context: Context, type: ts.ConditionalType): boolean { + return !!(type.flags & ts.TypeFlags.Conditional); + } + + /** + * Convert the given conditional type node to its type reflection. + * + * This is a node based converter, see [[convertType]] for the type equivalent. + * + * @param context The context object describing the current state the converter is in. + * @param node The conditional or intersection type node that should be converted. + * @returns The type reflection representing the given conditional type node. + */ + convertNode(context: Context, node: ts.ConditionalTypeNode): ConditionalType { + return new ConditionalType( + this.owner.convertType(context, node.checkType), + this.owner.convertType(context, node.extendsType), + this.owner.convertType(context, node.trueType), + this.owner.convertType(context, node.falseType) + ); + } + + /** + * Convert the given conditional type to its type reflection. + * + * This is a type based converter, see [[convertNode]] for the node equivalent. + * + * @param context The context object describing the current state the converter is in. + * @param type The conditional type that should be converted. + * @returns The type reflection representing the given conditional type. + */ + convertType(context: Context, type: ts.ConditionalType): ConditionalType { + return new ConditionalType( + this.owner.convertType(context, null, type.checkType), + this.owner.convertType(context, null, type.extendsType), + this.owner.convertType(context, null, type.resolvedTrueType), + this.owner.convertType(context, null, type.resolvedFalseType) + ); + } +} diff --git a/src/lib/converter/types/index.ts b/src/lib/converter/types/index.ts index 599bede9c..94431b68c 100644 --- a/src/lib/converter/types/index.ts +++ b/src/lib/converter/types/index.ts @@ -2,6 +2,7 @@ export { AliasConverter } from './alias'; export { ArrayConverter } from './array'; export { BindingArrayConverter } from './binding-array'; export { BindingObjectConverter } from './binding-object'; +export { ConditionalConverter } from './conditional'; export { EnumConverter } from './enum'; export { IntrinsicConverter } from './intrinsic'; export { StringLiteralConverter } from './string-literal'; diff --git a/src/lib/converter/types/union-or-intersection.ts b/src/lib/converter/types/union-or-intersection.ts index e9e256b7d..a2b04a8a9 100644 --- a/src/lib/converter/types/union-or-intersection.ts +++ b/src/lib/converter/types/union-or-intersection.ts @@ -47,7 +47,7 @@ export class UnionOrIntersectionConverter extends ConverterTypeComponent impleme /** * Convert the given union type to its type reflection. * - * This is a type based converter, see [[convertUnionTypeNode]] for the node equivalent. + * This is a type based converter, see [[convertNode]] for the node equivalent. * * ``` * let someValue: string|number; diff --git a/src/lib/models/types/conditional.ts b/src/lib/models/types/conditional.ts new file mode 100644 index 000000000..552a4ff7b --- /dev/null +++ b/src/lib/models/types/conditional.ts @@ -0,0 +1,72 @@ +import { Type } from './abstract'; + +/** + * Represents a conditional type. + * + * ~~~ + * let value: C extends E ? T : F; + * let value2: Check extends Extends ? True : False; + * ~~~ + */ +export class ConditionalType extends Type { + /** + * The type name identifier. + */ + readonly type: string = 'conditional'; + + constructor( + public checkType: Type, + public extendsType: Type, + public trueType: Type, + public falseType: Type + ) { + super(); + } + + /** + * Clone this type. + * + * @return A clone of this type. + */ + clone(): Type { + return new ConditionalType(this.checkType, this.extendsType, this.trueType, this.falseType); + } + + /** + * Test whether this type equals the given type. + * + * @param type The type that should be checked for equality. + * @returns TRUE if the given type equals this type, FALSE otherwise. + */ + equals(type: any): boolean { + if (!(type instanceof ConditionalType)) { + return false; + } + return this.checkType.equals(type.checkType) && + this.extendsType.equals(type.extendsType) && + this.trueType.equals(type.trueType) && + this.falseType.equals(type.falseType); + } + + /** + * Return a raw object representation of this type. + * @deprecated Use serializers instead + */ + toObject(): any { + const result: any = super.toObject(); + + result.checkType = this.checkType.toObject(); + result.extendsType = this.extendsType.toObject(); + result.trueType = this.trueType.toObject(); + result.falseType = this.falseType.toObject(); + + return result; + } + + /** + * Return a string representation of this type. + */ + toString() { + return this.checkType + ' extends ' + this.extendsType + ' ? ' + this.trueType + ' : ' + this.falseType; + } +} diff --git a/src/lib/models/types/index.ts b/src/lib/models/types/index.ts index b8dccc31d..91caa5a4e 100644 --- a/src/lib/models/types/index.ts +++ b/src/lib/models/types/index.ts @@ -1,5 +1,6 @@ export { Type } from './abstract'; export { ArrayType } from './array'; +export { ConditionalType } from './conditional'; export { IntrinsicType } from './intrinsic'; export { IntersectionType } from './intersection'; export { ReferenceType } from './reference'; diff --git a/src/lib/serialization/serializers/types/conditional.ts b/src/lib/serialization/serializers/types/conditional.ts new file mode 100644 index 000000000..031f22055 --- /dev/null +++ b/src/lib/serialization/serializers/types/conditional.ts @@ -0,0 +1,24 @@ +import { Component } from '../../../utils/component'; +import { ConditionalType } from '../../../models'; +import { TypeSerializerComponent } from '../../components'; + +@Component({name: 'serializer:conditional-type'}) +export class ConditionalTypeSerializer extends TypeSerializerComponent { + + initialize(): void { + super.initialize(); + this.supports = (t: ConditionalType) => t instanceof ConditionalType; + } + + toObject(conditional: ConditionalType, obj?: any): any { + obj = obj || {}; + + obj.checkType = this.owner.toObject(conditional.checkType); + obj.extendsType = this.owner.toObject(conditional.extendsType); + obj.trueType = this.owner.toObject(conditional.trueType); + obj.falseType = this.owner.toObject(conditional.falseType); + + return obj; + } + +} diff --git a/src/lib/serialization/serializers/types/index.ts b/src/lib/serialization/serializers/types/index.ts index 6131bdf3e..c8aed11d4 100644 --- a/src/lib/serialization/serializers/types/index.ts +++ b/src/lib/serialization/serializers/types/index.ts @@ -1,5 +1,6 @@ export * from './abstract'; export * from './array'; +export * from './conditional'; export * from './intersection-union'; export * from './intrinsic'; export * from './reference'; diff --git a/src/test/converter/conditional-type/conditional-type.ts b/src/test/converter/conditional-type/conditional-type.ts index b61e4a288..8b4141157 100644 --- a/src/test/converter/conditional-type/conditional-type.ts +++ b/src/test/converter/conditional-type/conditional-type.ts @@ -1,5 +1,9 @@ -function A (x: T): T extends number ? number : string { - return 'number' === typeof x ? x : x.toString() as any; +class C { + foo: number; +} + +function A (x: T): T extends number ? number : C { + return 'number' === typeof x ? x : new C() as any; } type B = T extends string ? 'string' : 'notstring'; diff --git a/src/test/converter/conditional-type/specs.json b/src/test/converter/conditional-type/specs.json index ff1133cf9..b614cb28b 100644 --- a/src/test/converter/conditional-type/specs.json +++ b/src/test/converter/conditional-type/specs.json @@ -15,14 +15,57 @@ "originalName": "%BASE%/conditional-type/conditional-type.ts", "children": [ { - "id": 6, + "id": 2, + "name": "C", + "kind": 128, + "kindString": "Class", + "flags": {}, + "children": [ + { + "id": 3, + "name": "foo", + "kind": 1024, + "kindString": "Property", + "flags": {}, + "sources": [ + { + "fileName": "conditional-type.ts", + "line": 2, + "character": 7 + } + ], + "type": { + "type": "intrinsic", + "name": "number" + } + } + ], + "groups": [ + { + "title": "Properties", + "kind": 1024, + "children": [ + 3 + ] + } + ], + "sources": [ + { + "fileName": "conditional-type.ts", + "line": 1, + "character": 7 + } + ] + }, + { + "id": 8, "name": "B", "kind": 4194304, "kindString": "Type alias", "flags": {}, "typeParameter": [ { - "id": 7, + "id": 9, "name": "T", "kind": 131072, "kindString": "Type parameter", @@ -32,41 +75,56 @@ "sources": [ { "fileName": "conditional-type.ts", - "line": 5, + "line": 9, "character": 6 } ], "type": { - "type": "unknown", - "name": "B" + "type": "conditional", + "checkType": { + "type": "typeParameter", + "name": "T" + }, + "extendsType": { + "type": "intrinsic", + "name": "string" + }, + "trueType": { + "type": "stringLiteral", + "value": "string" + }, + "falseType": { + "type": "stringLiteral", + "value": "notstring" + } } }, { - "id": 2, + "id": 4, "name": "A", "kind": 64, - "kindString": "Function", "flags": {}, "signatures": [ { - "id": 3, + "id": 5, "name": "A", "kind": 4096, - "kindString": "Call signature", "flags": {}, "typeParameter": [ { - "id": 4, + "id": 6, "name": "T", "kind": 131072, + "kindString": "Type parameter", "flags": {} } ], "parameters": [ { - "id": 5, + "id": 7, "name": "x", "kind": 32768, + "kindString": "Parameter", "flags": {}, "type": { "type": "typeParameter", @@ -75,33 +133,55 @@ } ], "type": { - "type": "unknown", - "name": "T extends number ? number : string" + "type": "conditional", + "checkType": { + "type": "typeParameter", + "name": "T" + }, + "extendsType": { + "type": "intrinsic", + "name": "number" + }, + "trueType": { + "type": "intrinsic", + "name": "number" + }, + "falseType": { + "type": "reference", + "name": "C" + } } } ], "sources": [ { - "fileName": "conditional-type.ts", - "line": 1, + "fileName": "%BASE%/conditional-type/conditional-type.ts", + "line": 5, "character": 10 } ] } ], "groups": [ + { + "title": "Classes", + "kind": 128, + "children": [ + 2 + ] + }, { "title": "Type aliases", "kind": 4194304, "children": [ - 6 + 8 ] }, { "title": "Functions", "kind": 64, "children": [ - 2 + 4 ] } ],