Skip to content

Commit

Permalink
added interface type for type checking.
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed May 8, 2021
1 parent da2025f commit f8a32bf
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 12 deletions.
208 changes: 208 additions & 0 deletions src/types/InterfaceType.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { expect } from 'chai';
import { assert } from 'sinon';
import type { BscType } from './BscType';
import { DynamicType } from './DynamicType';
import { IntegerType } from './IntegerType';
import { InterfaceType } from './InterfaceType';
import { ObjectType } from './ObjectType';
import { StringType } from './StringType';

describe('InterfaceType', () => {
describe('toString', () => {
it('returns empty curly braces when no members', () => {
expect(iface({}).toString()).to.eql('{}');
});

it('includes member types', () => {
expect(iface({ name: new StringType() }).toString()).to.eql('{ name: string; }');
});

it('includes nested object types', () => {
expect(
iface({
name: new StringType(),
parent: iface({
age: new IntegerType()
})
}
).toString()
).to.eql('{ name: string; parent: { age: integer; }; }');
});
});

describe('isConvertibleTo', () => {
it('works', () => {
expectAssignable({
name: new StringType()
}, {
name: new StringType()
});
});
});

describe('equals', () => {
it('matches equal objects', () => {
expect(
iface({ name: new StringType() }).equals(iface({ name: new StringType() }))
).to.be.true;
});

it('does not match inequal objects', () => {
expect(
iface({ name: new StringType() }).equals(iface({ name: new IntegerType() }))
).to.be.false;
});
});

describe('isAssignableTo', () => {
it('rejects being assignable to other types', () => {
expect(
iface({
name: new StringType()
}).isAssignableTo(new IntegerType())
).to.be.false;
});

it('matches exact properties', () => {
expectAssignable({
name: new StringType()
}, {
name: new StringType()
});
});

it('matches an object with more properties being assigned to an object with less', () => {
expectAssignable({
name: new StringType()
}, {
name: new StringType(),
age: new IntegerType()
});
});

it('rejects assigning an object with less properties to one with more', () => {
expectNotAssignable({
name: new StringType(),
age: new IntegerType()
}, {
name: new StringType()
});
});

it('matches properties in mismatched order', () => {
expect(
new InterfaceType(new Map([
['name', new StringType()],
['age', new IntegerType()]
])).isAssignableTo(new InterfaceType(new Map([
['age', new IntegerType()],
['name', new StringType()]
])))
).to.be.true;
});

it('rejects with member having mismatched type', () => {
expectNotAssignable({
name: new StringType()
}, {
name: new IntegerType()
});
});

it('rejects with object member having mismatched type', () => {
expectNotAssignable({
parent: iface({
name: new StringType()
})
}, {
parent: iface({
name: new IntegerType()
})
});
});

it('rejects with object member having missing prop type', () => {
expectNotAssignable({
parent: iface({
name: new StringType(),
age: new IntegerType()
})
}, {
parent: iface({
name: new StringType()
})
});
});

it('accepts with object member having same prop types', () => {
expectAssignable({
parent: iface({
name: new StringType(),
age: new IntegerType()
})
}, {
parent: iface({
name: new StringType(),
age: new IntegerType()
})
});
});

it('accepts with source member having dyanmic prop type', () => {
expectAssignable({
parent: iface({
name: new StringType(),
age: new IntegerType()
})
}, {
parent: new DynamicType()
});
});

it('accepts with target member having dyanmic prop type', () => {
expectAssignable({
parent: new DynamicType()
}, {
parent: iface({
name: new StringType(),
age: new IntegerType()
})
});
});

it('accepts with target member having "object" prop type', () => {
expectAssignable({
parent: new ObjectType()
}, {
parent: iface({
name: new StringType(),
age: new IntegerType()
})
});
});
});
});

function iface(members: Record<string, BscType>) {
return new InterfaceType(
new Map(
Object.entries(members)
)
);
}

function expectAssignable(targetMembers: Record<string, BscType>, sourceMembers: Record<string, BscType>) {
const targetIface = iface(targetMembers);
const sourceIface = iface(sourceMembers);
if (!sourceIface.isAssignableTo(targetIface)) {
assert.fail(`expected type ${targetIface.toString()} to be assignable to type ${sourceIface.toString()}`);
}
}

function expectNotAssignable(targetMembers: Record<string, BscType>, sourceMembers: Record<string, BscType>) {
const targetIface = iface(targetMembers);
const sourceIface = iface(sourceMembers);
if (sourceIface.isAssignableTo(targetIface)) {
assert.fail(`expected type ${targetIface.toString()} to not be assignable to type ${sourceIface.toString()}`);
}
}
60 changes: 51 additions & 9 deletions src/types/InterfaceType.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,70 @@
import { isDynamicType, isInterfaceType } from '../astUtils/reflection';
import { isDynamicType, isInterfaceType, isObjectType } from '../astUtils/reflection';
import type { BscType } from './BscType';

export class InterfaceType implements BscType {
public constructor(
public members: Map<string, BscType>
) {

}

/**
* The name of the interface. Can be null.
*/
public name: string;

public isAssignableTo(targetType: BscType) {
return (
isInterfaceType(targetType) ||
isDynamicType(targetType)
);
//we must have all of the members of the target type, and they must be equivalent types
if (isInterfaceType(targetType)) {
for (const [targetMemberName, targetMemberType] of targetType.members) {
//we don't have the target member
if (!this.members.has(targetMemberName)) {
return false;
}
//our member's type is not assignable to the target member type
if (!this.members.get(targetMemberName).isAssignableTo(targetMemberType)) {
return false;
}
}
//we have all of the target member's types. we are assignable!
return true;

//we are always assignable to dynamic or object
} else if (isDynamicType(targetType) || isObjectType(targetType)) {
return true;

//not assignable to any other object types
} else {
return false;
}
}

public isConvertibleTo(targetType: BscType) {
return this.isAssignableTo(targetType);
}

public toString() {
//TODO make this match the actual interface of the object
return 'interface';
let result = '{';
for (const [key, type] of this.members.entries()) {
result += ' ' + key + ': ' + type.toString() + ';';
}
if (this.members.size > 0) {
result += ' ';
}
return result + '}';
}

public toTypeString(): string {
return this.toString();
return 'object';
}

public equals(targetType: BscType): boolean {
return isInterfaceType(targetType);
if (isInterfaceType(targetType)) {
if (targetType.members.size !== this.members.size) {
return false;
}
return targetType.isAssignableTo(this);
}
return false;
}
}
3 changes: 0 additions & 3 deletions src/types/UninitializedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import type { BscType } from './BscType';

export class UninitializedType implements BscType {

// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor() { }

public isAssignableTo(targetType: BscType) {
return (
isUninitializedType(targetType) ||
Expand Down

0 comments on commit f8a32bf

Please sign in to comment.