Skip to content

Commit 67897e9

Browse files
authored
Improve the OOP example on the typechecking documentation. (#49)
1 parent 19c8be7 commit 67897e9

File tree

1 file changed

+26
-22
lines changed

1 file changed

+26
-22
lines changed

_pages/typecheck.md

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ type A = Callback<(number, string), ...number>
421421

422422
## Adding types for faux object oriented programs
423423

424-
One common pattern we see with existing Lua/Luau code is the following OO code. While Luau is capable of inferring a decent chunk of this code, it cannot pin down on the types of `self` when it spans multiple methods.
424+
One common pattern we see with existing Lua/Luau code is the following object-oriented code. While Luau is capable of inferring a decent chunk of this code, it cannot pin down on the types of `self` when it spans multiple methods.
425425

426426
```lua
427427
local Account = {}
@@ -450,49 +450,53 @@ local account = Account.new("Alexander", 500)
450450

451451
For example, the type of `Account.new` is `<a, b>(name: a, balance: b) -> { ..., name: a, balance: b, ... }` (snipping out the metatable). For better or worse, this means you are allowed to call `Account.new(5, "hello")` as well as `Account.new({}, {})`. In this case, this is quite unfortunate, so your first attempt may be to add type annotations to the parameters `name` and `balance`.
452452

453-
There's the next problem: the type of `self` is not shared across methods of `Account`, this is because you are allowed to explicitly opt for a different value to pass as `self` by writing `account.deposit(another_account, 50)`. As a result, the type of `Account:deposit` is `<a, b>(self: { balance: a }, credit: b) -> ()`. Consequently, Luau cannot infer the result of the `+` operation from `a` and `b`, so a type error is reported.
453+
There's the next problem: the type of `self` is not shared across methods of `Account`, this is because you are allowed to explicitly opt for a different value to pass as `self` by writing `account.deposit(another_account, 50)`. As a result, the type of `Account:deposit` is `<a, b>(self: { balance: a }, credit: b) -> ()`. Consequently, Luau cannot infer the result of the `+` operation from `a` and `b`, so a type error is reported.
454454

455-
We can see there's a lot of problems happening here. This is a case where you will have to guide Luau, but using the power of top-down type inference you only need to do this in _exactly one_ place!
455+
We can see there's a lot of problems happening here. This is a case where you'll have to provide some guidance to Luau in the form of annotations today, but the process is straightforward and without repetition. You first specify the type of _data_ you want your class to have, and then you define the class type separately with `setmetatable` (either via `typeof`, or in the New Type Solver, the `setmetatable` type function).
456+
From then on, you can explicitly annotate the `self` type of each method with your class type! Note that while the definition is written e.g. `Account.deposit`, you can still call it as `account:deposit(...)`.
456457

457458
```lua
458-
type AccountImpl = {
459-
__index: AccountImpl,
460-
new: (name: string, balance: number) -> Account,
461-
deposit: (self: Account, credit: number) -> (),
462-
withdraw: (self: Account, debit: number) -> (),
459+
local Account = {}
460+
Account.__index = Account
461+
462+
type AccountData = {
463+
name: string,
464+
balance: number,
463465
}
464466

465-
type Account = typeof(setmetatable({} :: { name: string, balance: number }, {} :: AccountImpl))
467+
export type Account = typeof(setmetatable({} :: AccountData, Account))
468+
-- or alternatively, in the new type solver...
469+
-- export type Account = setmetatable<AccountData, typeof(Account)>
466470

467-
-- Only these two annotations are necessary
468-
local Account: AccountImpl = {} :: AccountImpl
469-
Account.__index = Account
470471

471-
-- Using the knowledge of `Account`, we can take in information of the `new` type from `AccountImpl`, so:
472-
-- Account.new :: (name: string, balance: number) -> Account
473-
function Account.new(name, balance)
472+
-- this return annotation is not required, but ensures that you cannot
473+
-- accidentally make the constructor incompatible with the methods
474+
function Account.new(name, balance): Account
474475
local self = {}
475476
self.name = name
476477
self.balance = balance
477478

478479
return setmetatable(self, Account)
479480
end
480481

481-
-- Ditto:
482-
-- Account:deposit :: (self: Account, credit: number) -> ()
483-
function Account:deposit(credit)
482+
-- this annotation on `self` is the only _required_ annotation.
483+
function Account.deposit(self: Account, credit)
484+
-- autocomplete on `self` works here!
484485
self.balance += credit
485486
end
486487

487-
-- Ditto:
488-
-- Account:withdraw :: (self: Account, debit: number) -> ()
489-
function Account:withdraw(debit)
488+
-- this annotation on `self` is the only _required_ annotation.
489+
function Account.withdraw(self: Account, debit)
490+
-- autocomplete on `self` works here!
490491
self.balance -= debit
491492
end
492493

493-
local account = Account.new("Alexander", 500)
494+
local account = Account.new("Hina", 500)
495+
account:deposit(20) -- this still works, and we had autocomplete after hitting `:`!
494496
```
495497

498+
Based on feedback, we plan to restrict the types of all functions defined with `:` syntax to [share their self types](https://rfcs.luau.org/shared-self-types.html). This will enable future versions of this code to work without any explicit `self` annotations because it amounts to having type inference make precisely the assumptions we are encoding with annotations here --- namely, that the type of the constructors and the method definitions is intended by the developer to be the same.
499+
496500
## Tagged unions
497501

498502
Tagged unions are just union types! In particular, they're union types of tables where they have at least _some_ common properties but the structure of the tables are different enough. Here's one example:

0 commit comments

Comments
 (0)