Using TypeScript in Construct

Introductory video

If you want a quick video introduction to using TypeScript in Construct, see this YouTube video.

Subscribe to Construct videos now

Using TypeScript in Construct

Construct supports writing code for your project in both JavaScript and TypeScript. JavaScript is normally dynamically typed, meaning variables can have any type, and change to other types at any time - such as assigning a string to a variable containing a number. TypeScript however is an extension of JavaScript that makes it statically typed, meaning variables have a specific type, and cannot normally be changed to other types - so assigning a string to a variable containing a number becomes an error.

For example here is a typical variable declaration in JavaScript. It has no specific type, and it is allowed to be assigned to a different type entirely later on.

let x = 0;

// In JavaScript the variable can be changed to a string
x = "some string";

In TypeScript, a specific type can be set for the variable. Then trying to change the type becomes an error.

// In TypeScript, this variable is set to a number type
let x: number = 0;

// Now changing the type is an error
x = "some string";
// Error: Type 'string' is not assignable to type 'number'.

The main benefit to using TypeScript is it provides much more information for tools while writing code. For example Construct does provide an autocomplete for JavaScript code, but since JavaScript is dynamically typed, it doesn't know what properties are really available and so it tends to list everything in the autocomplete box. On the other hand TypeScript provides enough information for tools like VS Code to show an exact autocomplete list while writing code, which is much more helpful. This also allows a range of other tools, such as navigating source code, refactoring, and identifying errors.

As TypeScript is an extension of JavaScript, it is essentially the full JavaScript language, but with additional features on top. This guide assumes some familiarity with JavaScript. It also does not attempt to teach TypeScript in full, as it's already well documented. This guide covers the details about using TypeScript that are specific to Construct. Here are some additional links to help you get started with JavaScript and TypeScript:

Installation

To use TypeScript in Construct, you will need a TypeScript-compatible code editor. This guide uses Visual Studio Code, or VS Code for short. It's a free download and a professional coding tool used widely in the industry. This is then used as an external code editor for the code in your Construct project.

Install VS Code using the link above if you don't already have it. You'll also need to install TypeScript support, which you can do by following these steps:

  1. Install Node.js if you don't already have it
  2. In a terminal, run the command npm install -g typescript

You can check the TypeScript compiler, or tsc for short, is installed by running tsc --version in the terminal. It should print the version installed.

For more details see TypeScript in Visual Studio Code.

Setting up a Construct project

To use TypeScript with a Construct project, you must first save the project as a folder. This means it's made up of individual files in a folder, rather than everything being contained within a single .c3p file. To do this, choose MenuProjectSave asSave as project folder... and choose a folder to save the project to. If you're starting from scratch, you can also create a new project and then save it to a folder.

For more details on folder-based projects, see the manual section on Saving projects.

Next, right-click on the Scripts folder in the Project Bar, and select TypeScriptSet up TypeScript. This only needs to be done once per project and performs the initial setup for using TypeScript in your project. It does three things:

  1. It creates a TypeScript (.ts) file copy of every JavaScript (.js) file in your project. The TypeScript files aren't shown in the Project Bar, but they'll appear in your project folder. If the project doesn't have any JavaScript files, Construct creates the initial two default script files main.js and importsForEvents.js, and then creates TypeScript copies of those.
  2. A file named tsconfig.json is created in the project folder. This is a configuration file for TypeScript. This also does not appear in the Project Bar, as it's just for the external code editor.
  3. A subfolder named ts-defs is created with lots of .d.ts files. These are TypeScript definition files, which tells TypeScript about all of Construct's built-in APIs, as well as some types that are specific to your project. This also doesn't appear in the Project Bar.

If you run Set up TypeScript again, it won't overwrite any existing tsconfig.json or .ts files. This means it's safe to run again later on, for example if you add more .js files and want a quick way to make .ts copies of them.

Adding types

Now your project is ready for working with TypeScript! Start up VS Code, choose Open folder... and select your project folder (or the scripts subfolder if you prefer to see only your code). Open one of the TypeScript (.ts) files and you can start writing TypeScript.

The first thing that will happen is some errors will appear. While TypeScript is an extension of JavaScript, by default it requires types to be specified, since that is largely the point of using TypeScript. You will have to add types to fix the errors. Once you have fixed all the errors in all TypeScript files, it means your code is fully annotated with type information. If you're starting from scratch there's only a couple of updates, but if you already have a large amount of JavaScript code, there could be a significant amount of work to do to add types.

For example the default main.js code file includes this function:

async function OnBeforeProjectStart(runtime)
{
	// ...
}

TypeScript will identify the runtime parameter as an error, because it does not have a required type annotation. Construct's runtime interface type is IRuntime. So the parameter must be marked as having the type IRuntime, as shown below.

async function OnBeforeProjectStart(runtime: IRuntime)
{
	// ...
}

In many cases TypeScript can automatically infer the types of things so they don't necessarily always need to be updated. However there are several places like function parameters that will need type annotations to be added. Familiarity with Construct's built-in class names is useful as they are sometimes needed as types, such as IRuntime above - all the class names are included in the reference in the scripting section of the manual.

Other TypeScript changes

This guide does not contain an exhaustive list of all changes you'll need to make, but here are some common ones that you're likely to run in to, and some advice specific to Construct.

Instance types

When using TypeScript, Construct generates a special class representing an instance for every object type and family in the project. This includes type definitions for things like the instance variables, behaviors and effects specific to that object. These classes are all in the InstanceType namespace with the name of the object. For example InstanceType.Player is the type for an instance of the Player object type.

Optional types

Many of Construct methods, such as objectType.getFirstInstance(), can return null (in this case, if no instances exist at all). This means the method's return type can optionally be null. TypeScript will show an error if you try to use something that could be null. An example of this is shown below.

const playerInst = runtime.objects.Player.getFirstInstance();
playerInst.x += 10; // Error: 'playerInst' is possibly 'null'

If you know for sure that there is always an instance of the object and so it will never return null, you can add an exclamation mark ! after the expression to tell TypeScript you know it won't be null.

// Note '!' added to line below
const playerInst = runtime.objects.Player.getFirstInstance()!;
playerInst.x += 10; // OK

This is known as the non-null assertion operator.

Subclassing

If your project uses subclassing to customize the instance class for Construct objects, then you'll find some Construct APIs still return instances of the default type. For example instances of a Monster sprite object will be typed as the default InstanceType.Monster instead of a custom MonsterInstance class, e.g.:

const inst = runtime.objects.Monster.getFirstInstance()!;
// 'inst' is of type InstanceType.Monster - so it won't have any of
// the properties or methods of the custom MonsterInstance class

To solve this, the methods available on IObjectType are in fact generic, so you can make them return the correct type. This means adding the <Type> generic syntax like so:

const inst = runtime.objects.Monster.getFirstInstance<MonsterInstance>()!;
// 'inst' is now of type MonsterInstance and so can use the properties
// and methods of the custom class

Object literals

Sometimes it's useful to write an object literal, which the Ghost Shooter Code example does for sharing global variables from a module, similar to this:

const Globals = {
	score: 0,
	playerInstance: null
};

In this case, TypeScript will correctly infer the type of score as number, but it will infer the type of playerInstance as null. The type null means the variable can only ever have the value null and assigning anything else to it will be an error! Due to the syntax of object literals, which already use a colon, it's not always obvious at first how to add a specific type to this property. The solution is to use the generic-style syntax <Type> like so:

const Globals = {
	score: 0,
	playerInstance: <InstanceType.Player | null> null
};

Class properties

Normally in JavaScript, class properties are added in the constructor.

class MyClass {
	constructor()
	{
		this.prop1 = "hello";
		this.prop2 = 123;
	}
}

TypeScript does not infer the class properties from the constructor, so it will show an error for prop1 and prop2, e.g. Property 'prop1' does not exist on type 'MyClass'. Instead you must declare the class properties, and their types, at the class-level like so:

class MyClass {

	prop1: string;
	prop2: number;

	constructor()
	{
		this.prop1 = "hello";
		this.prop2 = 123;
	}
}

Note JavaScript does allow class property definitions in a similar way, with the feature known as class fields. Using this feature in JavaScript should make it easier to switch to TypeScript, as you can then just add type annotations to the existing class fields.

Imports

Typically when importing other JavaScript files in your project, you'd write a relative import for another .js file like so:

import Globals from "./globals.js";

How do you write the import for TypeScript? The answer is: exactly the same way! Even though the import ends with .js, TypeScript knows the file is really generated from the .ts file, and so everything just works. Don't try to change it, as otherwise it won't work after it's compiled to JavaScript.

Example

The Spell Caster TypeScript example demonstrates the Spell Caster Code JavaScript example but updated to use TypeScript. In particular, this commit shows the list of changes that were necessary to add types to the existing JavaScript code.

Workflow

Once you are up and running, you will likely want to make repeated changes to your TypeScript code, and easily be able to preview the result in Construct. To make this process work smoothly, use the following two settings:

  1. In VS Code, press Ctrl + Shift + B, and then select tsc: watch. This enables a mode where VS Code will automatically compile your .ts files to .js whenever you save the file. Note this must be done once per session.
  2. In Construct, right-click on the Scripts folder in the Project Bar and choose Auto reload all on preview. (Construct remembers this setting across sessions.)

The workflow then goes like this:

  1. You make a change to a TypeScript file and save the change
  2. TypeScript then automatically compiles the .ts file to .js (or reports errors if you made a mistake)
  3. Then preview the project in Construct, at which point the .js file is re-loaded from the project folder

Sometimes you may make changes to the project that affect the TypeScript definition files Construct generated for you. Alternatively when updating to new versions of Construct, the TypeScript definition files could change too. To make sure the TypeScript definition files are up-to-date, right-click on the Scripts folder in the Project Bar, and select TypeScriptUpdate TypeScript definitions. This essentially does only step 3 from the Set up TypeScript steps.

Conclusion

TypeScript is a great way to enhance the coding experience in Construct, using the same industry-standard tooling and languages as used by many professionals. It requires using a folder-based project and an external editor like VS Code, but in turn it brings useful features such as precise autocomplete, error checking, and source code navigation.

See the Spell Caster TypeScript example on GitHub for a sample of a JavaScript project converted to TypeScript. There is also excellent official TypeScript documentation available which is a great place to go to learn more about TypeScript.

Construct 3 Manual 2024-07-25