Skip to content

Commit 44cbe51

Browse files
authored
Add new docs about assigning variables to global scope (#186)
* Add new docs abbout assigning variables to global scope * fix typo * review comments
1 parent a7bf9f9 commit 44cbe51

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

docs/assigning-global-variables.md

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
title: Setting global variables or functions
3+
---
4+
5+
import { SideBySide } from "@site/src/components/SideBySide";
6+
7+
In some Lua environments, the host application expects you to define global variables or functions as part of the API. For example, some engines might allow you to define some event handlers in your Lua, that will be called by the engine when different events happen:
8+
9+
```lua title=example.lua
10+
function OnStart()
11+
-- start event handling code
12+
end
13+
14+
function OnStateChange(newState)
15+
-- state change event handler code
16+
end
17+
```
18+
19+
Due to the way TSTL translates module code, functions will be `local` after translation, causing the engine not to find them:
20+
21+
<SideBySide>
22+
23+
```typescript title=input.ts
24+
function OnStart(this: void) {
25+
// start event handling code
26+
}
27+
function OnStateChange(this: void, newState: State) {
28+
// state change event handler code
29+
}
30+
```
31+
32+
```lua title=output.lua
33+
local function OnStart()
34+
end
35+
local function OnStateChange(newState)
36+
end
37+
```
38+
39+
</SideBySide>
40+
41+
This means we need extra helper code to correctly register these global variables so your environment can access them.
42+
43+
## Assigning to globals with declarations
44+
45+
The easiest way to set global variables is to first declare them as existing globals, and then assign their values. As an example:
46+
47+
```ts
48+
declare var OnStart: (this: void) => void;
49+
declare var OnStateChanged: (this: void, newState: State) => void;
50+
51+
OnStart = () => {
52+
// start event handling code
53+
};
54+
OnStateChanged = (newState: State) => {
55+
// state change event handler code
56+
};
57+
```
58+
59+
In the example above, the declarations are in the same file as the value assignments. Alternatively, you could choose to put them in a .d.ts file included in your project. If these globals have pre-defined names specified by the engine the API, it is also possible to include these declarations in the .d.ts files (or types package) for this environment.
60+
61+
## Setting global variables with a helper function
62+
63+
Another way to assign global variables and functions is to use a helper function. The benefit is that you can strictly type the helper functions to ensure correct types are assigned, as well as having the ability to do additional logic like wrapping or modifying the value.
64+
65+
The simplest helper function looks like this:
66+
67+
```typescript
68+
function registerEventHandler<TArgs extends unknown[]>(
69+
handlerName: string,
70+
handler: (this: void, ...args: TArgs) => void,
71+
): void {
72+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
73+
globalThis[handlerName] = handler;
74+
}
75+
```
76+
77+
This helper function can be added in some shared TypeScript helper file and imported wherever you need it.
78+
79+
You can now write the example code like this:
80+
81+
```typescript
82+
registerEventHandler("OnStart", () => {
83+
// start event handling code
84+
});
85+
registerEventHandler("OnStateChanged", (newState: State) => {
86+
// state change event handler code
87+
});
88+
```
89+
90+
Of course you can modify `registerEventHandler` to your needs if you need to assign variables of different types to the global scope. For example, you could add a second `register` function for assigning non-function values if needed:
91+
92+
```typescript
93+
function registerGlobalVariable<T>(variableName: string, value: T): void {
94+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
95+
globalThis[handlerName] = value;
96+
}
97+
```
98+
99+
## Registering functions as class methods with a decorator
100+
101+
Sometimes you don't want to register just a loose function, but instead register a class or class method. A nice way to do this is to use decorators (they unfortunately only work on classes, and not for loose functions).
102+
103+
One example of such a decorator is:
104+
105+
```typescript
106+
function registerEventHandler<TReturn, TArgs extends unknown[]>(
107+
method: (...args: TArgs) => TReturn,
108+
context: ClassMethodDecoratorContext,
109+
) {
110+
/** @noSelf - the engine will not pass self parameter so wrap in lambda without self */
111+
const contextless = (...args: TArgs) => method(...args);
112+
// We can read the name of the method from the context
113+
const globalName = context.name;
114+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
115+
globalThis[globalName] = contextless;
116+
}
117+
```
118+
119+
You can then write above example as:
120+
121+
```typescript
122+
class EventHandlers {
123+
@registerEventHandler
124+
public OnStart() {
125+
// start event handling code
126+
}
127+
@registerEventHandler
128+
public OnStateChanged(newState: State) {
129+
// state change event handler code
130+
}
131+
}
132+
```
133+
134+
:::note
135+
In the above example, `this` will be `nil` in the methods, do not try to use other members in the EventHandlers class!
136+
:::
137+
138+
## Registering classes with a decorator
139+
140+
Sometimes you want to register classes instead of functions, you can also do that with a decorator:
141+
142+
```typescript
143+
function registerClass<TClass, TArgs extends unknown[]>(
144+
c: new (...args: TArgs) => TClass,
145+
context: ClassDecoratorContext,
146+
) {
147+
if (context.name) {
148+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
149+
globalThis[context.name] = c;
150+
}
151+
}
152+
```
153+
154+
You can now register any class by simply adding the decorator:
155+
156+
```typescript
157+
@registerClass
158+
class EventHandlers {
159+
public OnStart() {
160+
// start event handling code
161+
}
162+
public OnStateChanged(newState: State) {
163+
// state change event handler code
164+
}
165+
}
166+
```
167+
168+
### Custom global name decorator
169+
170+
In the examples above, the decorators directly used the name of the decorated class or method, but with decorator parameters you can also specify custom override names:
171+
172+
```typescript
173+
const registerClass =
174+
(globalName: string) =>
175+
<TClass, TArgs extends unknown[]>(c: new (...args: TArgs) => TClass, context: ClassDecoratorContext) => {
176+
if (context.name) {
177+
// @ts-ignore tell TS to ignore us 'illegally' writing to global scope
178+
globalThis[globalName] = c;
179+
}
180+
};
181+
```
182+
183+
Now instead of taking the global name from the class, you can specify a custom name yourself:
184+
185+
```typescript
186+
@registerClass("CustomGlobalName")
187+
class EventHandlers {
188+
public OnStart() {
189+
// start event handling code
190+
}
191+
public OnStateChanged(newState: State) {
192+
// state change event handler code
193+
}
194+
}
195+
```

sidebars.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"caveats",
1010
"the-self-parameter",
1111
"advanced/writing-declarations",
12+
"assigning-global-variables",
1213
"external-code",
1314
"publishing-modules",
1415
"editor-support"

0 commit comments

Comments
 (0)