Skip to content

Commit

Permalink
fix(type): allow using (self) function type reference
Browse files Browse the repository at this point in the history
fixes #354
  • Loading branch information
marcj committed Dec 2, 2022
1 parent d46c35e commit 7191c58
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 11 deletions.
20 changes: 14 additions & 6 deletions packages/type-compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ import {
TypeReferenceNode,
UnionTypeNode,
visitEachChild,
visitNode
visitNode, isVariableDeclaration, isTypeNode
} from 'typescript';
import {
ensureImportIsEmitted,
Expand Down Expand Up @@ -1693,11 +1693,11 @@ export class ReflectionTransformer implements CustomTransformer {
* This is a custom resolver based on populated `locals` from the binder. It uses a custom resolution algorithm since
* we have no access to the binder/TypeChecker directly and instantiating a TypeChecker per file/transformer is incredible slow.
*/
protected resolveDeclaration(typeName: EntityName): { declaration: Declaration, importDeclaration?: ImportDeclaration, typeOnly?: boolean } | void {
protected resolveDeclaration(typeName: EntityName): { declaration: Node, importDeclaration?: ImportDeclaration, typeOnly?: boolean } | void {
let current: Node = typeName.parent;
if (typeName.kind === SyntaxKind.QualifiedName) return; //namespace access not supported yet, e.g. type a = Namespace.X;

let declaration: Declaration | undefined = undefined;
let declaration: Node | undefined = undefined;

while (current) {
if (isNodeWithLocals(current) && current.locals) {
Expand Down Expand Up @@ -1878,7 +1878,15 @@ export class ReflectionTransformer implements CustomTransformer {
return;
}

const declaration = resolved.declaration;
let declaration: Node = resolved.declaration;
if (isVariableDeclaration(declaration)) {
if (declaration.type) {
declaration = declaration.type;
} else if (declaration.initializer) {
declaration = declaration.initializer;
}
}

if (isModuleDeclaration(declaration) && resolved.importDeclaration) {
if (isIdentifier(typeName)) ensureImportIsEmitted(resolved.importDeclaration, typeName);

Expand Down Expand Up @@ -2023,7 +2031,7 @@ export class ReflectionTransformer implements CustomTransformer {
// //<Type>{[Property in keyof Type]: boolean;};
// this.extractPackStructOfType(declaration, program);
// return;
} else if (isClassDeclaration(declaration)) {
} else if (isClassDeclaration(declaration) || isFunctionDeclaration(declaration) || isFunctionExpression(declaration) || isArrowFunction(declaration)) {
//if explicit `import {type T}`, we do not emit an import and instead push any
if (resolved.typeOnly) {
program.pushOp(ReflectionOp.any);
Expand All @@ -2039,7 +2047,7 @@ export class ReflectionTransformer implements CustomTransformer {
}
const body = isIdentifier(typeName) ? typeName : this.createAccessorForEntityName(typeName);
const index = program.pushStack(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, body));
program.pushOp(ReflectionOp.classReference, index);
program.pushOp(isClassDeclaration(declaration) ? ReflectionOp.classReference : ReflectionOp.functionReference, index);
program.popFrameImplicit();
} else if (isTypeParameterDeclaration(declaration)) {
this.resolveTypeParameter(declaration, type, program);
Expand Down
24 changes: 24 additions & 0 deletions packages/type-compiler/tests/transpile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,30 @@ test('es2021', () => {
expect(res.app).toContain(`const type = typeOf([], [() => __ΩReadUser, 'n!'])`);
});

test('Return function ref', () => {
//see GitHub issue #354
const res = transpile({
'app': `
function Option<T>(val: T): Option<T> {
};
`
});
console.log(res);
expect(res.app).toContain(`() => Option,`);
});

test('Return arrow function ref', () => {
//see GitHub issue #354
const res = transpile({
'app': `
const Option = <T>(val: T): Option<T> => {
};
`
});
console.log(res);
expect(res.app).toContain(`() => Option,`);
});

//currently knownLibFilesForCompilerOptions from @typescript/vfs doesn't return correct lib files for ES2022
// test('es2022', () => {
// const res = transpile({
Expand Down
2 changes: 2 additions & 0 deletions packages/type-spec/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,6 @@ export enum ReflectionOp {

static,
mappedType2, //same as mappedType2 but the given functionPointer returns a tuple [type, name]

functionReference, //Same as classReference but for functions
}
15 changes: 10 additions & 5 deletions packages/type/src/reflection/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,20 +629,25 @@ export class Processor {
this.pushType(t);
break;
}
case ReflectionOp.functionReference:
case ReflectionOp.classReference: {
const ref = this.eatParameter() as number;
const classType = resolveFunction(program.stack[ref] as Function, program.object);
const classOrFunction = resolveFunction(program.stack[ref] as Function, program.object);
const inputs = this.popFrame() as Type[];
if (!classType) throw new Error('No class reference given in ' + String(program.stack[ref]));
if (!classOrFunction) throw new Error('No class/function reference given in ' + String(program.stack[ref]));

if (!classType.__type) {
this.pushType({ kind: ReflectionKind.class, classType, typeArguments: inputs, types: [] });
if (!classOrFunction.__type) {
if (op === ReflectionOp.classReference) {
this.pushType({ kind: ReflectionKind.class, classType: classOrFunction, typeArguments: inputs, types: [] });
} else if (op === ReflectionOp.functionReference) {
this.pushType({ kind: ReflectionKind.function, function: classOrFunction, parameters: [], return: { kind: ReflectionKind.unknown } });
}
} else {

//when it's just a simple reference resolution like typeOf<Class>() then enable cache re-use (so always the same type is returned)
const reuseCached = !!(this.isEnded() && program.previous && program.previous.end === 0);

const result = this.reflect(classType, inputs, { reuseCached });
const result = this.reflect(classOrFunction, inputs, { reuseCached });
this.push(result, program);

if (isWithAnnotations(result) && inputs.length) {
Expand Down
40 changes: 40 additions & 0 deletions packages/type/tests/type.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,46 @@ test('ignore constructor in keyof', () => {
expect(type.types.map(v => v.kind === ReflectionKind.literal ? v.literal : 'unknown')).toEqual(['foo', 'id']);
});

test('function returns self reference', () => {
type Option<T> = OptionType<T>;

class OptionType<T> {
constructor(val: T, some: boolean) {
}
}

function Option<T>(val: T): Option<T> {
return new OptionType(val, true);
}

const type = reflect(Option);
assertType(type, ReflectionKind.function);
expect(type.function).toBe(Option);
assertType(type.return, ReflectionKind.function);
expect(type.return.function).toBe(Option);
});

test('arrow function returns self reference', () => {
type Option<T> = OptionType<T>;

class OptionType<T> {
constructor(val: T, some: boolean) {
}
}

const Option = <T>(val: T): Option<T> => {
return new OptionType(val, true);
}

const type = reflect(Option);
assertType(type, ReflectionKind.function);
expect(type.function).toBe(Option);

//we need to find out why TS does resolve Option<T> in arrow function to the class and not the variable
assertType(type.return, ReflectionKind.class);
expect(type.return.classType).toBe(OptionType);
});

class User {
a!: string;
}
Expand Down

0 comments on commit 7191c58

Please sign in to comment.