Forcing Crafty Through TypeScript
11/18/15
So, as I've said, I'm playing with TypeScript while interfacing with Crafty.
This has already been quite interesting, and hopefully will provide dividends down the line with some deeper Crafty API understanding.
And already I'm finding myself doing... questionable things with TypeScript, and I'm not sure if it's TypeScript's typeness, or Crafty's craftiness.
Let me show you what I got so far.
The definition file:
// Type definitions for craftyjs
// Project: http://craftyjs.com/
// Definitions by: Gregory "Rory" Ksionda <https://github.com/ksiondag>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
declare module CraftyJS {
interface Static {
(selector: string): AnyEntity;
(id: number): AnyEntity;
init(width: number, height: number, divId: string): void;
e(...components: string[]): AnyEntity;
c(name: string, definition: Component): void;
}
interface Component {
// Stuff Crafty itself checks for
init?(): void;
required?: string;
// Components are user-defined objects that plug into entities
// Very flexible creation
[index: string]: any;
}
/*
All entities have these properties
*/
interface Core<T> {
addComponent(...components: string[]): void;
requires(components: string): void;
// Setting values
attr(property: string, value: any, silent?: boolean, recursive?: boolean): T;
attr(map: Object, silent?: boolean, recursive?: boolean): T;
// Getting values
attr(property: string): any;
}
interface TextComponent<T> extends Core<T> {
text(value: string): T;
textFont(value: TextFont): T;
}
/*
Entities could have any number of defined functions depending on their
components
*/
interface AnyEntity extends TextComponent<AnyEntity>, Core<AnyEntity> {
}
interface TextFont {
size?: string
}
}
declare var Crafty: CraftyJS.Static;
My typescript_experiment.ts file:
/// <reference path="../../../Crafty/crafty.d.ts" />
module CraftyJS {
export interface Static {
e(component: 'Player'): PlayerEntity;
}
export interface PlayerComponent<T> extends TextComponent<T>, Core<T> {
}
export interface PlayerEntity extends PlayerComponent<PlayerEntity> {
}
export interface AnyEntity extends PlayerComponent<AnyEntity> {
}
}
(function () {
var player: CraftyJS.PlayerEntity;
Crafty.init(600, 900, 'game');
Crafty.c('Player', {
init: function () {
var self: CraftyJS.PlayerEntity = this;
self.addComponent('DOM', '2D', 'Text');
}
});
player = Crafty.e('Player')
.attr({
x: 0,
y: 0,
h: 900,
w: 600
})
.text('Made with TypeScript')
.textFont({
size: '40px'
});
})();
Now, this code has already gone through quite a few iterations since inception. All of which compiled to basically the same JavaScript code without errors.
This structure is the structure I like best so far, but it still has problems.
But let's talk about the stuff I like first.
interface Static {
(selector: string): AnyEntity;
(id: number): AnyEntity;
init(width: number, height: number, divId: string): void;
e(...components: string[]): AnyEntity;
c(name: string, definition: Component): void;
}
...
declare var Crafty: CraftyJS.Static;
This is a subset straight from the
API documentation,
and it works quite nicely. All the calls to the functions in
typescript_experiment.ts
compile as they should.
Now, then, the stuff I struggled with:
/*
All entities have these properties
*/
interface Core<T> {
addComponent(...components: string[]): void;
requires(components: string): void;
// Setting values
attr(property: string, value: any, silent?: boolean, recursive?: boolean): T;
attr(map: Object, silent?: boolean, recursive?: boolean): T;
// Getting values
attr(property: string): any;
}
interface TextComponent<T> extends Core<T> {
text(value: string): T;
textFont(value: TextFont): T;
}
/*
Entities could have any number of defined functions depending on their
components
*/
interface AnyEntity extends TextComponent<AnyEntity>, Core<AnyEntity> {
}
I'm having a lot of trouble translating Crafty's component-based system into an intelligent and DRY type setup.
This is the closest I've gotten so far. An entity extends the components it's made up of (along with the core that all entities have).
Components are defined with Generics. This was the only way I could set it up so that chaining retains the entity's set type.
Hopefully soon
there'll be a
this
type
I can use that'll fix the main problem.
But I think I would also like some means of telling TypeScript that calling a function extends a type. Something like:
interface Core<T> {
addComponent(component: 'Player'): T extends PlayerComponent<T>;
}
Being able to do more than one extend with a splat definition would be cool, too.
interface Static {
e(...components: string[], 'Player', ...components: string[]): extends PlayerComponent<T>;
e(...components: string[], 'Text', ...components: string[]): extends TextComponent<T>;
e(...components: string[]): Entity;
}
Where extends in this context would specify that the base return type of the function is extended by each match of the other function definitions.
But perhaps that's kinda a weird thing to want out of the static typing. I'm not familiar enough with JavaScript idioms and TypeScript's ideal considerations of JavaScript.
If I want to gain a better understanding, though, it's probably worth
it to bring it up as an issue like that
this
link above.
Anyways, I'm still quite enjoying TypeScript, though I'm letting myself get too sucked into semantics, as fun as I find it.
The next post should be something that can be properly be played with.