mirror of
https://github.com/luau-lang/luau.git
synced 2025-05-04 10:33:46 +01:00
RFC: User-defined type predicates
This commit is contained in:
parent
1fa8311a18
commit
24185678fe
1 changed files with 133 additions and 0 deletions
133
rfcs/syntax-type-predicates.md
Normal file
133
rfcs/syntax-type-predicates.md
Normal file
|
@ -0,0 +1,133 @@
|
|||
# User-defined Type Predicates
|
||||
|
||||
## Summary
|
||||
|
||||
Support user-defined type predicates, which are runtime functions which a user can create and be used to refine the type
|
||||
of a particular expression in the type checker.
|
||||
|
||||
## Motivation
|
||||
|
||||
The Luau type engine already provides built-in support for type predicates. In vanilla Luau, this can be seen
|
||||
when using `typeof(X) == "type"`. Host environments can also specify their own predicate functions, such as
|
||||
`Instance:IsA("ClassName")` in Roblox.
|
||||
|
||||
These predicate functions can then be used in combination with type guards or `assert(X)` statements (and, more recently, control flow analysis).
|
||||
For example:
|
||||
|
||||
```lua
|
||||
local x: unknown = ...
|
||||
if typeof(x) == "string" then
|
||||
-- x is a string
|
||||
x ..= "value"
|
||||
end
|
||||
```
|
||||
|
||||
Right now, there is currently no way for a user to define their own predicate functions in code.
|
||||
If a user defines their own type, it cannot be easily refined through the type checker, without using a type assertion
|
||||
or just "assuming" the type is already correct. This is particularly prevelant in environments such as unsecure network code.
|
||||
|
||||
Consider the following function which is called by a uncontrolled client (i.e., any arguments can actually be passed into it):
|
||||
|
||||
```lua
|
||||
type Pet = {
|
||||
name: string,
|
||||
age: number,
|
||||
}
|
||||
|
||||
local function processClientEvent(pet: unknown)
|
||||
-- there is no way here to assert that pet is of type Pet
|
||||
end
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
We propose a new syntax for return types in a function: `variable :: type`, where `variable` must match
|
||||
the name of a function parameter. In context:
|
||||
|
||||
```lua
|
||||
type Pet = {
|
||||
name: string,
|
||||
age: number,
|
||||
}
|
||||
|
||||
local function isPet(x: unknown): x :: Pet
|
||||
return typeof(x) == "table" and typeof(x.name) == "string" and typeof(x.age) == "number"
|
||||
end
|
||||
```
|
||||
|
||||
Function type syntax can follow a similar structure:
|
||||
|
||||
```lua
|
||||
type isPet = (x: unknown) -> x :: Pet
|
||||
```
|
||||
|
||||
We can then use this in practice:
|
||||
|
||||
```lua
|
||||
local function processClientEvent(pet: unknown)
|
||||
assert(isPet(pet), "invalid argument #1 to 'processClientEvent' (expected 'Pet')")
|
||||
-- here we now know that pet is of type Pet
|
||||
print(pet.name) -- no error
|
||||
print(pet.age) -- no error
|
||||
print(pet.size) -- TypeError
|
||||
end
|
||||
```
|
||||
|
||||
User defined type predicates can also be used in type guards, and control flow:
|
||||
|
||||
```lua
|
||||
local function processClientEvent(pet: unknown)
|
||||
if isPet(pet) then
|
||||
-- pet is "Pet"
|
||||
end
|
||||
-- pet is "unknown"
|
||||
end
|
||||
|
||||
local function processClientEvent(pet: unknown)
|
||||
if not isPet(pet) then return end
|
||||
-- pet is "Pet"
|
||||
end
|
||||
```
|
||||
|
||||
## Drawbacks
|
||||
|
||||
One problem with user-defined type predicates, is that they do not completely solve the unsoundness issue, if the user
|
||||
incorrectly defines their type predicate function.
|
||||
|
||||
For example:
|
||||
|
||||
```lua
|
||||
type Pet = {
|
||||
name: string,
|
||||
age: number,
|
||||
size: number,
|
||||
}
|
||||
|
||||
local function isPet(x: unknown): x :: Pet
|
||||
-- bug: we have not verified that x contains a property "size"
|
||||
return typeof(x) == "table" and typeof(x.name) == "string" and typeof(x.age) == "number"
|
||||
end
|
||||
|
||||
local function processClientEvent(pet: unknown)
|
||||
assert(isPet(pet))
|
||||
pet.size += 5 -- runtime error: attempt to perform arithmetic (add) on nil and number
|
||||
end
|
||||
|
||||
processClientEvent({ name = "Cat", age = 5 })
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
We could follow the syntax of [TypeScript type predicates](https://www.typescriptlang.org/docs/handbook/advanced-types.html):
|
||||
|
||||
```lua
|
||||
local function isPet(x: unknown): x is Pet
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
|
||||
This syntax could be feasible, with `is` being a context-sensitive keyword. We choose the type assertion operator `::`
|
||||
to remain consistent with other syntax, and follow the idea that we are "asserting" the type of a parameter.
|
||||
|
||||
To fix the unsoundness drawback above, Luau could instead automatically generate type predicates for all custom defined user
|
||||
types.
|
Loading…
Add table
Reference in a new issue