Skip to content
This repository has been archived by the owner on Jul 19, 2019. It is now read-only.
Mikael Hermansson edited this page Apr 4, 2019 · 19 revisions

Here are some implementation details and inherent quirks with mapping TypeScript to Blueprint that you should probably be aware of if you're going to develop with TypeScript for Unreal.

Table of Contents

Self parameters

In case you get tired of constantly wiring the Self node to your TSU functions, you can enable the Use Self Parameter setting in Project Settings -> Plugins -> TypeScript for Unreal. Whatever name you put into Self Parameter Name will have the DefaultToSelf metadata assigned to it, which means wiring that parameter will become optional.

Note that you can not use the parameter name self or this, because those are reserved by Blueprint and TypeScript respectively. Also note that changing these settings mean you'll have to recompile all your existing TSU blueprints.

Immutable types

Certain types in Blueprint, such as Vector, Rotator and Transform are marked as immutable. You'll see this represented in their typings as having readonly properties. This means that they can never be modified.

In Blueprint itself this never really becomes an issue, since you always "break" and "make" those types if you want to modify a specific property on them, meaning you always create new instances instead of modifying existing ones.

In TypeScript however this leads to something as simple as player.position.x = 5 becoming disallowed, and instead you have to do player.position = player.position.withX(5);.

Reference equality

Because of how TSU is implemented, equality between references can be potentially dangerous. So something like...

export function test(player: BP_Player) {
    // Don't do this
    if (player.mesh === player.mesh) {
        console.log('Success!');
    }
}

... might seem harmless (and nonsensical), but can in some cases fail. This is because .mesh is a property, and will inside its getter create a new object (sometimes), meaning the equality check can fail. What you should do instead is rely on any equality method that might be available, like...

export function test(player: BP_Player) {
    // Do this instead
    if (player.mesh.equals(player.mesh)) {
        console.log('Success!');
    }
}

Also note that because of this, using references as keys for Map is inadvisable, as is using Array methods like indexOf or includes when dealing with arrays of references.

Extension methods

One of the fundamental features of TSU is what's commonly referred to as "extension methods" in other languages. They allow you to extend the API of an existing class without directly modifying it. TSU implements this by assuming that any method inside a UBlueprintFunctionLibrary is an extension to the type of its first argument.

As an example, let's take the Vector2D type. Without any extension methods at all, the typings for it would look something like:

declare class Vector2D {
    readonly x: number;
    readonly y: number;
}

... and then you would have to use the operations found in KismetMathLibrary:

export function translate(origin: Vector2D, offset: Vector2D) {
    return KismetMathLibrary.addVector2D(origin, offset);
}

This can get quite verbose after a while, and doesn't look very nice when you start chaining operations. TSU therefore takes all the library methods that has Vector2D as its first argument and adds them to the Vector2D type, like so:

declare class Vector2D {
    readonly x: number;
    readonly y: number;

    /** Returns addition of Vector A and Vector B (A + B) */
    add(b: Vector2D): Vector2D;

    // Plus a bunch more extension methods...
}

As you can see the first argument is gone, and the comment doesn't perfectly reflect the parameters anymore. Also, the name of the method has been trimmed down to remove redundancy. Now we're now able to do this instead:

export function translate(origin: Vector2D, offset: Vector2D) {
    return origin.add(offset);
}

Debugging proxies

While debugging you might encounter certain Blueprint properties where its type is Proxy. This is due to implementation details with regards to struct and array properties. At runtime proxies are completely transparent, but when debugging you will (unfortunately) see its real Proxy type.

If you're already familiar with proxies you might be inclined to look at its [[Target]] in hopes of seeing the actual underlying property, but you should instead look for the actualObject or actualArray property inside the [[Handler]] of the proxy.

Example of a struct property (with the desired property marked in red):

struct example

Example of an array property (with the desired property marked in red):

array example

Module-scope state

You might be tempted to store state in the module-scope of a file, like...

let count = 0;
export function foo(thing: BP_Thing) {
    console.log(count++);
}

... but keep in mind that the context in which things are executed might get reset at any moment (due to hot-reloading or other factors). Exported functions in TSU are assumed to be "pure", in the sense that they don't rely on any persistent state within the JavaScript context itself. Instead, do something like...

export function foo(thing: BP_Thing) {
    console.log(thing.count++);
}

"Why is [some function] not available?"

Certain nodes/functions in Blueprint are bespoke, and not available in the reflection API that TSU relies on. This include nodes like Add Static Mesh Component, Timeline and Delay.

The workaround for components/timelines is to do something like...

const component = new TimelineComponent(parent);
component.registerComponent();
// ...
parent.addOwnedComponent(component);

In the case of Delay, you can instead use the global function setTimeout. Keep in mind though that Delay in Blueprint will simply stop the execution if there is an ongoing delay, whereas setTimeout will create an additional delay instead.

Also remember that you can always wrap missing functionality inside a Blueprint function and call that from TypeScript instead.

If there's anything you feel is missing or is inconvenient then please feel free to open an issue about it.

Web/Node.js APIs

TSU only implements a minimal subset of the APIs that JavaScript developers might be familiar with, like console.log and setTimeout. As such, stuff like WebSocket or other web APIs are not available. The same applies to any Node.js APIs you might usually reach for, like fs, path or process.

Reacting to a hot-reload

If for whatever reason you would like to perform something at the time of a hot-reload, you can have your Blueprint class implement the TsuHotReloadListenerInterface under Edit Class Settings. This will give you access to the Post Hot Reload event in your Blueprint graph.