Skip to content

Commit

Permalink
feature(type): implement call signatures in object literals.
Browse files Browse the repository at this point in the history
Fix: function no longer extends non-empty object literal in runtime.

fixes #429
  • Loading branch information
marcj committed Apr 12, 2023
1 parent e740430 commit ece8c02
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 18 deletions.
16 changes: 10 additions & 6 deletions packages/type-compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ArrayTypeNode,
ArrowFunction,
Bundle,
CallSignatureDeclaration,
ClassDeclaration,
ClassElement,
ClassExpression,
Expand Down Expand Up @@ -51,6 +52,7 @@ import {
isArrayTypeNode,
isArrowFunction,
isCallExpression,
isCallSignatureDeclaration,
isClassDeclaration,
isClassExpression,
isConstructorDeclaration,
Expand Down Expand Up @@ -1463,14 +1465,15 @@ export class ReflectionTransformer implements CustomTransformer {
case SyntaxKind.ConstructSignature:
case SyntaxKind.ConstructorType:
case SyntaxKind.FunctionType:
case SyntaxKind.CallSignature:
case SyntaxKind.FunctionDeclaration: {
//TypeScript does not narrow types down
const narrowed = node as MethodSignature | MethodDeclaration | ConstructorTypeNode | ConstructSignatureDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | FunctionTypeNode | FunctionDeclaration;
const narrowed = node as MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorTypeNode | ConstructSignatureDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | FunctionTypeNode | FunctionDeclaration;

const config = this.findReflectionConfig(narrowed, program);
if (config.mode === 'never') return;

const name = isConstructorTypeNode(narrowed) || isConstructSignatureDeclaration(node) ? 'new' : isConstructorDeclaration(narrowed) ? 'constructor' : getPropertyName(this.f, narrowed.name);
const name = isCallSignatureDeclaration(node) ? '' : isConstructorTypeNode(narrowed) || isConstructSignatureDeclaration(node) ? 'new' : isConstructorDeclaration(narrowed) ? 'constructor' : getPropertyName(this.f, narrowed.name);
if (!narrowed.type && narrowed.parameters.length === 0 && !name) return;

program.pushFrame();
Expand Down Expand Up @@ -1509,10 +1512,11 @@ export class ReflectionTransformer implements CustomTransformer {
}

program.pushOp(
isMethodSignature(narrowed) || isConstructSignatureDeclaration(narrowed)
? ReflectionOp.methodSignature
: isMethodDeclaration(narrowed) || isConstructorDeclaration(narrowed)
? ReflectionOp.method : ReflectionOp.function, program.findOrAddStackEntry(name)
isCallSignatureDeclaration(node) ? ReflectionOp.callSignature :
isMethodSignature(narrowed) || isConstructSignatureDeclaration(narrowed)
? ReflectionOp.methodSignature
: isMethodDeclaration(narrowed) || isConstructorDeclaration(narrowed)
? ReflectionOp.method : ReflectionOp.function, program.findOrAddStackEntry(name)
);

if (isMethodDeclaration(narrowed)) {
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 @@ -210,4 +210,6 @@ export enum ReflectionOp {
mappedType2, //same as mappedType2 but the given functionPointer returns a tuple [type, name]

functionReference, //Same as classReference but for functions

callSignature, //Same as function but for call signatures (in object literals)
}
11 changes: 8 additions & 3 deletions packages/type/src/reflection/extends.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
isType,
isTypeIncluded,
ReflectionKind,
resolveTypeMembers,
Type,
TypeAny,
TypeInfer,
Expand Down Expand Up @@ -124,7 +125,7 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
if (left.kind === ReflectionKind.null || left.kind === ReflectionKind.undefined) return false;
if (right.types.length === 0) {
//string extends {}, number extends {} are all valid
return left.kind === ReflectionKind.templateLiteral || isPrimitive(left);
return left.kind === ReflectionKind.templateLiteral || left.kind === ReflectionKind.function || isPrimitive(left);
}
}

Expand Down Expand Up @@ -174,9 +175,13 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
(right.kind === ReflectionKind.function || right.kind === ReflectionKind.method || right.kind === ReflectionKind.methodSignature || right.kind === ReflectionKind.objectLiteral)
) {
if (right.kind === ReflectionKind.objectLiteral) {
//todo: members maybe contain a call signature
for (const type of resolveTypeMembers(right)) {
if (type.kind === ReflectionKind.callSignature) {
if (isExtendable(left, type, extendStack)) return true;
}
}

return true;
return false;
}

if (right.kind === ReflectionKind.function || right.kind === ReflectionKind.methodSignature || right.kind === ReflectionKind.method) {
Expand Down
25 changes: 19 additions & 6 deletions packages/type/src/reflection/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
ReflectionVisibility,
Type,
TypeBaseMember,
TypeCallSignature,
TypeClass,
typeDecorators,
TypeEnumMember,
Expand Down Expand Up @@ -793,15 +794,25 @@ export class Processor {
this.handleIntersection();
break;
}
case ReflectionOp.callSignature:
case ReflectionOp.function: {
const types = this.popFrame() as Type[];
const name = program.stack[this.eatParameter() as number] as string;
let t: TypeFunction = {

const returnType = types.length > 0 ? types[types.length - 1] as Type : { kind: ReflectionKind.any } as Type;
const parameters = types.length > 1 ? types.slice(0, -1) as TypeParameter[] : [];

let t = op === ReflectionOp.callSignature ? {
kind: ReflectionKind.callSignature,
return: returnType,
parameters
} as TypeCallSignature : {
kind: ReflectionKind.function,
name: name || undefined,
return: types.length > 0 ? types[types.length - 1] as Type : { kind: ReflectionKind.any } as Type,
parameters: types.length > 1 ? types.slice(0, -1) as TypeParameter[] : []
};
return: returnType,
parameters
} as TypeFunction;

if (this.isEnded()) t = assignResult(program.resultType, t);
t.return.parent = t;
for (const member of t.parameters) member.parent = t;
Expand Down Expand Up @@ -905,7 +916,7 @@ export class Processor {
types: []
} as TypeObjectLiteral;

const frameTypes = this.popFrame() as (TypeIndexSignature | TypePropertySignature | TypeMethodSignature | TypeObjectLiteral)[];
const frameTypes = this.popFrame() as (TypeIndexSignature | TypePropertySignature | TypeMethodSignature | TypeObjectLiteral | TypeCallSignature)[];
pushObjectLiteralTypes(t, frameTypes);

//only for the very last op do we replace this.resultType. Otherwise, objectLiteral in between would overwrite it.
Expand Down Expand Up @@ -1755,7 +1766,7 @@ function applyPropertyDecorator(type: Type, data: TData) {

function pushObjectLiteralTypes(
type: TypeObjectLiteral,
types: (TypeIndexSignature | TypePropertySignature | TypeMethodSignature | TypeObjectLiteral)[],
types: (TypeIndexSignature | TypePropertySignature | TypeMethodSignature | TypeObjectLiteral | TypeCallSignature)[],
) {
let annotations: Annotations = {};
const decorators: Type[] = [];
Expand Down Expand Up @@ -1804,6 +1815,8 @@ function pushObjectLiteralTypes(
} else {
type.types.push(toAdd);
}
} else if (member.kind === ReflectionKind.callSignature) {
type.types.push(member);
}
}

Expand Down
17 changes: 14 additions & 3 deletions packages/type/src/reflection/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export enum ReflectionKind {
methodSignature,

infer,

callSignature,
}

export type TypeDecorator = (annotations: Annotations, decorator: TypeObjectLiteral) => boolean;
Expand Down Expand Up @@ -250,7 +252,7 @@ export interface TypeParameter extends TypeAnnotations {
kind: ReflectionKind.parameter,
name: string;
type: Type;
parent: TypeFunction | TypeMethod | TypeMethodSignature;
parent: TypeFunction | TypeMethod | TypeMethodSignature | TypeCallSignature;

//parameter could be a property as well if visibility is set
visibility?: ReflectionVisibility,
Expand Down Expand Up @@ -294,6 +296,13 @@ export interface TypeFunction extends TypeAnnotations {
return: Type;
}

export interface TypeCallSignature extends TypeAnnotations {
kind: ReflectionKind.callSignature,
parent?: Type;
parameters: TypeParameter[];
return: Type;
}

export interface TypePromise extends TypeAnnotations {
kind: ReflectionKind.promise,
parent?: Type;
Expand Down Expand Up @@ -388,7 +397,7 @@ export interface TypeMethodSignature extends TypeAnnotations {
export interface TypeObjectLiteral extends TypeAnnotations {
kind: ReflectionKind.objectLiteral,
parent?: Type;
types: (TypeIndexSignature | TypePropertySignature | TypeMethodSignature)[];
types: (TypeIndexSignature | TypePropertySignature | TypeMethodSignature | TypeCallSignature)[];
}

export interface TypeIndexSignature extends TypeAnnotations {
Expand Down Expand Up @@ -464,6 +473,7 @@ export type Type =
| TypeTupleMember
| TypeRest
| TypeRegexp
| TypeCallSignature
;

export type Widen<T> =
Expand Down Expand Up @@ -2044,7 +2054,7 @@ export function isCustomTypeClass(type: Type) {
/**
* Returns the members of a class or object literal.
*/
export function resolveTypeMembers(type: TypeClass | TypeObjectLiteral): (TypeProperty | TypePropertySignature | TypeMethodSignature | TypeMethod | TypeIndexSignature)[] {
export function resolveTypeMembers(type: TypeClass | TypeObjectLiteral): (TypeProperty | TypePropertySignature | TypeMethodSignature | TypeMethod | TypeIndexSignature | TypeCallSignature)[] {
return type.types;
}

Expand Down Expand Up @@ -2384,6 +2394,7 @@ export function stringifyType(type: Type, stateIn: Partial<StringifyTypeOptions>
stack.push({ type: type.type, depth: depth + 1 });
break;
}
case ReflectionKind.callSignature:
case ReflectionKind.function:
stack.push({ type: type.return, depth: depth + 1 });
stack.push({ before: ') => ' });
Expand Down
55 changes: 55 additions & 0 deletions packages/type/tests/type.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,3 +1398,58 @@ describe('types equality', () => {
}
}
});

test('function extends empty object literal', () => {
interface ObjectLiteral {
}

type isFunction = Function extends ObjectLiteral ? true : false;
expect(stringifyResolvedType(typeOf<isFunction>())).toBe('true');
});

test('call signature', () => {
interface ObjectLiteralWithCall {
(b: string): number;
}

const type = typeOf<ObjectLiteralWithCall>();
assertType(type, ReflectionKind.objectLiteral);
assertType(type.types[0], ReflectionKind.callSignature);
assertType(type.types[0].parameters[0], ReflectionKind.parameter);
expect(type.types[0].parameters[0].name).toBe('b');
assertType(type.types[0].parameters[0].type, ReflectionKind.string);
assertType(type.types[0].return, ReflectionKind.number);

expect(stringifyResolvedType(typeOf<ObjectLiteralWithCall>())).toBe(`ObjectLiteralWithCall {(b: string) => number}`);
});

test('function extends non-empty object literal', () => {
interface ObjectLiteral {
a: string;
}

type isFunction = Function extends ObjectLiteral ? true : false;
expect(stringifyResolvedType(typeOf<isFunction>())).toBe('false');
});

test('issue-429: invalid function detection', () => {
interface IDTOInner {
subfieldA: string;
subfieldB: number;
}

interface IDTOOuter {
fieldA: string;
fieldB: IDTOInner;
fieldC: number;

someFunction(): void;
}

type ObjectKeysMatching<O extends {}, V> = { [K in keyof O]: O[K] extends V ? K : V extends O[K] ? K : never }[keyof O];
type keys = ObjectKeysMatching<IDTOOuter, Function>;

type isFunction = Function extends IDTOInner ? true : false;
expect(stringifyResolvedType(typeOf<isFunction>())).toBe('false');
expect(stringifyResolvedType(typeOf<keys>())).toBe(`'someFunction'`);
});

0 comments on commit ece8c02

Please sign in to comment.