Skip to content

Commit

Permalink
feat: Convert conditional types, indexed access types
Browse files Browse the repository at this point in the history
closes #1150
closes #831 

* Added test for conditional types. i.e. `X extends A ? B : C`
  Currently results in { "type": "unknown", "name": "X extends A ? B : C" }
  See #831
* Add support for conditional type.
* add a test for infer
* feat: Add support for indexed access types

Co-authored-by: Adrian Leonhard <adrianleonhard@gmail.com>
  • Loading branch information
Gerrit0 and NaridaL committed Dec 26, 2019
1 parent 0551aeb commit 03987a0
Show file tree
Hide file tree
Showing 23 changed files with 982 additions and 36 deletions.
37 changes: 23 additions & 14 deletions examples/basic/src/generics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @param value A generic parameter.
* @returns A generic return value.
*/
function testFunction<T>(value:T):T {
function testFunction<T>(value: T): T {
return value;
}

Expand All @@ -14,41 +14,37 @@ function testFunction<T>(value:T):T {
*
* @param T The generic type parameter.
*/
interface A<T>
{
interface A<T> {
/**
* A generic member function.
*
* @return A generic return value.
*/
getT():T;
getT(): T;
}


/**
* A generic interface with two type parameters.
*
* @param <T> The first generic type parameter.
* @param <C> The second generic type parameter.
*/
interface B<T, C>
{
interface B<T, C> {
/**
* A generic member function.
*
* @param value A generic parameter.
*/
setT(value:T):void;
setT(value: T): void;

/**
* A generic member function.
*
* @return A generic return value.
*/
getC():C;
getC(): C;
}


/**
* A generic interface extending two other generic interfaces
* and setting one of the type parameters.
Expand All @@ -57,24 +53,37 @@ interface B<T, C>
*/
interface AB<T> extends A<T>, B<T, boolean> {}


/**
* An interface extending a generic interface and setting its type parameter.
*/
interface ABString extends AB<string> {}


/**
* An interface extending a generic interface and setting its type parameter.
*/
interface ABNumber extends AB<number> {}


/**
* A function returning a generic array with type parameters.
*
* @return The return value with type arguments.
*/
function getGenericArray():Array<string> {
function getGenericArray(): Array<string> {
return [''];
}

/**
* Conditional type with infer
*/
type PopFront<T extends any[]> = ((...args: T) => any) extends ((a: any, ...r: infer R) => any) ? R : never;

/**
* See GH#1150. Calling typeChecker.typeToString on this type will send TS into an infinite
* loop, which is undesirable.
*/
type HorribleRecursiveTypeThatShouldNotBeUsedByAnyone<T extends any[], R = {}> = {
0: R,
1: HorribleRecursiveTypeThatShouldNotBeUsedByAnyone<PopFront<T>, {
[K in keyof R | keyof T[0]]: K extends keyof R ? R[K] : T[0][K]
}>
}[T['length'] extends 0 ? 0 : 1];
9 changes: 8 additions & 1 deletion scripts/rebuild_specs.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check

const assert = require('assert');
const fs = require('fs-extra');
const path = require('path');
const TypeDoc = require('..');
Expand Down Expand Up @@ -49,7 +50,13 @@ function rebuildConverterTests(dirs) {
TypeDoc.resetReflectionID();
before();
const result = app.convert(src);
const data = JSON.stringify(result.toObject(), null, ' ')
// Until GH#936 lands, removing toObject, ensure toObject remains consistent
// with the serializers.
const serialized = result.toObject();
const serialized2 = app.serializer.toObject(result);
assert.deepStrictEqual(serialized, serialized2);

const data = JSON.stringify(serialized, null, ' ')
.split(TypeDoc.normalizePath(base))
.join('%BASE%');
after();
Expand Down
56 changes: 56 additions & 0 deletions src/lib/converter/types/conditional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as ts from 'typescript';

import { ConditionalType, Type } from '../../models/types';
import { Component, ConverterTypeComponent, TypeConverter } from '../components';
import { Context } from '../context';

@Component({name: 'type:conditional'})
export class ConditionalConverter extends ConverterTypeComponent implements TypeConverter<ts.ConditionalType, ts.ConditionalTypeNode> {
/**
* 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 | undefined {
const types = this.owner.convertTypes(context, [node.checkType, node.extendsType, node.trueType, node.falseType]);
if (types.length !== 4) {
return undefined;
}
return new ConditionalType(...types as [Type, Type, Type, Type]);
}

/**
* 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 | undefined {
const types = this.owner.convertTypes(context, [], [type.checkType, type.extendsType, type.resolvedTrueType, type.resolvedFalseType]);
if (types.length !== 4) {
return undefined;
}
return new ConditionalType(...types as [Type, Type, Type, Type]);
}
}
3 changes: 3 additions & 0 deletions src/lib/converter/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ 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 { InferredConverter } from './inferred';
export { IndexedAccessConverter } from './indexed-access';
export { IntrinsicConverter } from './intrinsic';
export { PredicateConverter } from './predicate';
export { StringLiteralConverter } from './string-literal';
Expand Down
28 changes: 28 additions & 0 deletions src/lib/converter/types/indexed-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as ts from 'typescript';

import { Type, IndexedAccessType } from '../../models/index';
import {
Component,
ConverterTypeComponent,
TypeNodeConverter
} from '../components';
import { Context } from '../context';

@Component({ name: 'type:indexed-access' })
export class IndexedAccessConverter extends ConverterTypeComponent
implements TypeNodeConverter<ts.Type, ts.IndexedAccessTypeNode> {
/**
* Test whether this converter can handle the given TypeScript node.
*/
supportsNode(context: Context, node: ts.TypeNode) {
return ts.isIndexedAccessTypeNode(node);
}

convertNode(context: Context, node: ts.IndexedAccessTypeNode): Type | undefined {
const objectType = this.owner.convertType(context, node.objectType);
if (!objectType) { return; }
const indexType = this.owner.convertType(context, node.indexType);
if (!indexType) { return; }
return new IndexedAccessType(objectType, indexType);
}
}
28 changes: 28 additions & 0 deletions src/lib/converter/types/inferred.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as ts from 'typescript';

import { InferredType } from '../../models/types';
import { Component, ConverterTypeComponent, TypeNodeConverter } from '../components';
import { Context } from '../context';

@Component({name: 'type:inferred'})
export class InferredConverter extends ConverterTypeComponent implements TypeNodeConverter<ts.Type, ts.InferTypeNode> {
/**
* Test whether this converter can handle the given TypeScript node.
*/
supportsNode(_context: Context, node: ts.TypeNode): boolean {
return ts.isInferTypeNode(node);
}

/**
* 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.InferTypeNode): InferredType | undefined {
return new InferredType(node.typeParameter.getText());
}
}
2 changes: 1 addition & 1 deletion src/lib/converter/types/union-or-intersection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,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;
Expand Down
72 changes: 72 additions & 0 deletions src/lib/models/types/conditional.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
3 changes: 3 additions & 0 deletions src/lib/models/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export { Type } from './abstract';
export { ArrayType } from './array';
export { ConditionalType } from './conditional';
export { IndexedAccessType } from './indexed-access';
export { InferredType } from './inferred';
export { IntersectionType } from './intersection';
export { IntrinsicType } from './intrinsic';
export { PredicateType } from './predicate';
Expand Down
61 changes: 61 additions & 0 deletions src/lib/models/types/indexed-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Type } from './index';

/**
* Represents an indexed access type.
*/
export class IndexedAccessType extends Type {
/**
* The type name identifier.
*/
readonly type = 'indexedAccess';

/**
* Create a new TupleType instance.
*
* @param elementType The type of the array's elements.
*/
constructor(public objectType: Type, public indexType: Type) {
super();
}

/**
* Clone this type.
*
* @return A clone of this type.
*/
clone(): Type {
return new IndexedAccessType(this.objectType, this.indexType);
}

/**
* 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: Type): boolean {
if (!(type instanceof IndexedAccessType)) {
return false;
}
return type.objectType.equals(this.objectType) && type.indexType.equals(this.indexType);
}

/**
* Return a raw object representation of this type.
* @deprecated Use serializers instead
*/
toObject(): any {
return {
...super.toObject(),
objectType: this.objectType.toObject(),
indexType: this.indexType.toObject()
};
}

/**
* Return a string representation of this type.
*/
toString() {
return `${this.objectType.toString()}[${this.indexType.toString()}]`;
}
}

0 comments on commit 03987a0

Please sign in to comment.