Skip to main content

Differences in the self Parameter

When using TypeScriptToLua (TSTL) to write TypeScript and compile it into Lua, developers need to be aware of some key differences in behavior due to language-specific features. This article will focus on handling the self parameter, managing context in callback functions, and how to avoid common type errors.

The Importance of the self Parameter

In the default configuration of TSTL, all functions include a self parameter by default to simulate the behavior of the this keyword in JavaScript. The self parameter refers to the object context calling the function. For most Lua users, this behavior is similar to calling methods using Lua's : (colon).

1. Considerations for Dora SSR

In Dora SSR, TSTL is configured with the noImplicitSelf option enabled. This means that ordinary functions will no longer include a self parameter by default to simulate JavaScript's this keyword behavior.

1.1 Example:

In Dora SSR:

Input (TypeScript)

function f() {}
function f2(this: any) {}
const a = () => {};
class C {
method() {}
};
interface Item {
method(): void;
};
const item: Item = {
method() {}
};

Output (Lua)

function f() end
function f2(self) end
local a = function() end

local C = __TS__Class()
C.name = "C"
function C.prototype.____constructor(self) end
function C.prototype.method(self) end -- Class methods still include the self parameter

local item = {
method = function(self) end -- Object member functions still include the self parameter
}

Even with noImplicitSelf enabled, class methods and object member functions still default to including self unless you explicitly declare this: void.

2. How to Explicitly Remove the self Parameter

If you want to remove the self parameter explicitly, especially when interacting with Lua code, you can use the TypeScript this: void syntax. This informs the compiler that this is not allowed in the current context, eliminating the self parameter.

2.1 Using this: void in Class Methods

In class definitions, you can declare this: void if you want certain methods to exclude the self parameter.

Example:

Input (TypeScript)

declare class Class {
colon(arg: string): void;
dot(this: void, arg: string): void;
}

const c = new Class();
c.colon("foo"); // Called with colon
c.dot("foo"); // Called with dot notation

Output (Lua)

local c = __TS__New(Class)
c:colon("foo")
c.dot("foo")

This way, you can control whether the self parameter is generated in class methods according to your needs.

2.2 Handling self in Callback Functions

In many Lua libraries, callback functions do not use the self parameter. When writing TypeScript code that interacts with such libraries, ensure that the callback function does not include the self parameter.

Example:

Input (TypeScript)

type Callback = (this: void, arg: string) => void;

declare function useCallback(this: void, callback: Callback): void;

useCallback(arg => {
print(arg);
});

Output (Lua)

useCallback(function(arg)
print(arg)
end)

In this example, we explicitly declare that the callback function does not include the self parameter, ensuring the generated Lua code does not have extra context parameters.

2.3 Using the @noSelf Annotation

If you want to ensure that all functions in a class or interface do not include the self parameter, you can use the @noSelf annotation. This can save you from manually specifying this: void for every function.

Input (TypeScript)

/** @noSelf **/
interface Item {
foo(arg: string): void;
};
const item: Item = {
foo(arg) {}
};

Output (Lua)

local item = {
foo = function(arg) end
}

You can also override the @noSelf annotation for individual functions by explicitly specifying the this parameter:

Input (TypeScript)

/** @noSelf **/
interface Item {
foo(arg: string): void;
bar(this: any, arg: string): void;
};
const item: Item = {
foo(arg) {},
bar(arg) {}
};

Output (Lua)

local item = {
foo = function(arg) end,
bar = function(self, arg) end
}

3. Assignment Errors and Solutions

In TSTL, functions with this: void and regular functions cannot be assigned to each other. If you try to assign a function with a context parameter to one without it, TSTL will throw an error.

Error Example:

declare function useCallback(cb: (this: void, arg: string) => void);

function callback(arg: string) {}
useCallback(callback); // ❌ Error

You can resolve this error by wrapping the function in an arrow function:

Corrected:

useCallback((arg) => callback(arg));

This ensures that TSTL does not generate a self parameter, avoiding type mismatches.

4. Avoiding Context Inconsistencies in Overloaded Functions

In TypeScript, functions can be overloaded with different signatures. However, in TSTL, if overloaded functions have inconsistent context types (e.g., one has this: void and another does not), it will result in a compilation error.

Example:

declare function useCallback(f: () => {}): void;

declare function callback(this: void, s: string, n: number): void;
declare function callback(s: string);

useCallback(callback); // ❌ Error: Inconsistent context types

To avoid these errors, it’s best to avoid overloading functions with inconsistent context types.