Type System
Type System
Section titled “Type System”Experimental. Some limitations are expected.
Wippy includes a gradual type system with flow-sensitive checking. Types are non-nullable by default.
Primitives
Section titled “Primitives”local n: number = 3.14local i: integer = 42 -- integer is subtype of numberlocal s: string = "hello"local b: boolean = truelocal a: any = "anything" -- explicit dynamic (opt-out of checking)local u: unknown = something -- must narrow before useany vs unknown
Section titled “any vs unknown”-- any: opt-out of type checkinglocal a: any = get_data()a.foo.bar.baz() -- no error, may crash at runtime
-- unknown: safe unknown, must narrow before use as a concrete typelocal u: unknown = get_data()u.foo -- no error: member access on unknown behaves like anylocal n: number = u -- ERROR: unknown not assignable to number, narrow firstif type(u) == "table" then -- u narrowed to table hereendNil Safety
Section titled “Nil Safety”Types are non-nullable by default. Use ? for optional values:
local x: number = nil -- ERROR: nil not assignable to numberlocal y: number? = nil -- OK: number? means "number or nil"local z: number? = 42 -- OKControl Flow Narrowing
Section titled “Control Flow Narrowing”The type checker tracks control flow:
local function process(x: number?): number if x ~= nil then return x -- x is number here end return 0end
-- Early return patternlocal user, err = get_user(123)if err then return nil, err end-- user narrowed to non-nil here
-- Or defaultlocal val = get_value() or 0 -- val: numberUnion Types
Section titled “Union Types”local val: number | string = get_value()
if type(val) == "number" then print(val + 1) -- val: numberelse print(val:upper()) -- val: stringendLiteral Types
Section titled “Literal Types”type Status = "pending" | "active" | "done"
local s: Status = "pending" -- OKlocal s: Status = "invalid" -- ERRORFunction Types
Section titled “Function Types”local function add(a: number, b: number): number return a + bend
-- Multiple returnslocal function div_mod(a: number, b: number): (number, number) return math.floor(a / b), a % bend
-- Error returns (Lua idiom)local function fetch(url: string): (string?, error?) -- returns (data, nil) or (nil, error)end
-- First-class function typeslocal double: (number) -> number = function(x: number): number return x * 2endVariadic Functions
Section titled “Variadic Functions”local function sum(...: number): number local total: number = 0 for _, v in ipairs({...}) do total = total + v end return totalendRecord Types
Section titled “Record Types”type User = {name: string, age: number}
local u: User = {name = "alice", age = 25}Optional Fields
Section titled “Optional Fields”type Config = { host: string, port: number, timeout?: number, debug?: boolean}
local cfg: Config = {host = "localhost", port = 8080} -- OKGenerics
Section titled “Generics”local function identity<T>(x: T): T return xend
local n: number = identity(42)local s: string = identity("hello")Constrained Generics
Section titled “Constrained Generics”type HasName = {name: string}
local function greet<T: HasName>(obj: T): string return "Hello, " .. obj.nameend
greet({name = "Alice"}) -- OKgreet({age = 30}) -- ERROR: missing 'name'Intersection Types
Section titled “Intersection Types”Combine multiple types:
type Named = {name: string}type Aged = {age: number}type Person = Named & Aged
local p: Person = {name = "Alice", age = 30}Tagged Unions
Section titled “Tagged Unions”type Result<T, E> = {ok: true, value: T} | {ok: false, error: E}
type LoadState = {status: "loading"} | {status: "loaded", data: User} | {status: "error", message: string}
local function render(state: LoadState): string if state.status == "loading" then return "Loading..." elseif state.status == "loaded" then return "Hello, " .. state.data.name elseif state.status == "error" then return "Error: " .. state.message endendThe never Type
Section titled “The never Type”never is the bottom type - no values exist:
function fail(msg: string): never error(msg)endError Handling Pattern
Section titled “Error Handling Pattern”The checker understands the Lua error idiom:
local value, err = call()if err then -- value is nil here return nil, errend-- value is non-nil here, err is nilprint(value)Non-Nil Assertion
Section titled “Non-Nil Assertion”Use ! to assert an expression is non-nil:
local user: User? = get_user()local name = (user!).name -- assert user is non-nil! is a type-checker assertion only - it narrows the type to non-nil but emits no runtime check. If the value is actually nil, the following operation fails with the usual error (e.g. indexing nil). Use when you know a value cannot be nil but the type checker cannot prove it.
Type Casts
Section titled “Type Casts”Safe Cast (Validation)
Section titled “Safe Cast (Validation)”Call a type as a function to validate and cast:
local data: any = get_json()local user = User(data) -- validates and returns Userlocal name = user.name -- safe field accessWorks with primitives and custom types:
local x: any = get_value()local s = string(x) -- cast to stringlocal n = integer(x) -- cast to integerlocal b = boolean(x) -- cast to boolean
type Point = {x: number, y: number}local p = Point(data) -- validates record structureType:is() Method
Section titled “Type:is() Method”Validate without throwing, returns (value, nil) or (nil, error):
type Point = {x: number, y: number}local data: any = get_input()
local p, err = Point:is(data)if p then local sum = p.x + p.y -- p is valid Pointelse return nil, err -- validation failedendThe result narrows in conditionals:
if Point:is(data) then local p: Point = data -- data narrowed to PointendUnsafe Cast
Section titled “Unsafe Cast”Use :: or as for unchecked casts:
local data: any = get_data()local user = data :: User -- no runtime checklocal user = data as User -- same as ::Use sparingly. Unsafe casts bypass validation and can cause runtime errors if the value doesn’t match the type.
Type Reflection
Section titled “Type Reflection”Types are first-class values with introspection methods.
Kind and Name
Section titled “Kind and Name”print(Number:kind()) -- "number"print(Point:kind()) -- "record"print(Point:name()) -- "Point"Record Fields
Section titled “Record Fields”Iterate over record fields:
type User = {name: string, age: number}
for name, typ in User:fields() do print(name, typ:kind())end-- name string-- age numberAccess individual field types:
local nameType = User.name -- type of 'name' fieldprint(nameType:kind()) -- "string"Collection Types
Section titled “Collection Types”local arr: {number} = {1, 2, 3}local arrType = typeof(arr)print(arrType:elem():kind()) -- "number"
local map: {[string]: number} = {}local mapType = typeof(map)print(mapType:key():kind()) -- "string"print(mapType:val():kind()) -- "number"Optional Types
Section titled “Optional Types”local opt: number? = nillocal optType = typeof(opt)print(optType:kind()) -- "optional"print(optType:inner():kind()) -- "number"Union Types
Section titled “Union Types”type Status = "pending" | "active" | "done"
for variant in Status:variants() do print(variant)endFunction Types
Section titled “Function Types”local fn: (number, string) -> boolean
local fnType = typeof(fn)for param in fnType:params() do print(param:kind())endprint(fnType:ret():kind()) -- "boolean"Type Comparison
Section titled “Type Comparison”print(Number == Number) -- trueprint(Integer <= Number) -- true (subtype)print(Integer < Number) -- true (strict subtype)Types as Table Keys
Section titled “Types as Table Keys”local handlers = {}handlers[Number] = function() return "number handler" endhandlers[String] = function() return "string handler" end
local h = handlers[typeof(value)]if h then h() endType Annotations
Section titled “Type Annotations”Add types to function signatures:
-- Parameter and return typeslocal function process(input: string): number return #inputend
-- Local variable typeslocal count: number = 0
-- Type aliasestype StringArray = {string}type StringMap = {[string]: number}Type Validators
Section titled “Type Validators”Add runtime validation constraints to types using annotations:
-- Single validatorlocal x: number @min(0) = 1
-- Multiple validatorslocal x: number @min(0) @max(100) = 50
-- String patternlocal email: string @pattern("^.+@.+$") = "test@example.com"Built-in Validators
Section titled “Built-in Validators”| Validator | Applies to | Example |
|---|---|---|
@min(n) | number | local x: number @min(0) = 1 |
@max(n) | number | local x: number @max(100) = 50 |
@min_len(n) | string, array | local s: string @min_len(1) = "hi" |
@max_len(n) | string, array | local s: string @max_len(10) = "hi" |
@pattern(regex) | string | local email: string @pattern("^.+@.+$") = "a@b.com" |
Record Field Validators
Section titled “Record Field Validators”type User = { age: number @min(0) @max(150), name: string @min_len(1) @max_len(100)}Array Element Validators
Section titled “Array Element Validators”local scores: {number @min(0) @max(100)} = {85, 90}Union Member Validators
Section titled “Union Member Validators”local id: number @min(1) | string @min_len(1) = 1Variance Rules
Section titled “Variance Rules”| Position | Variance | Description |
|---|---|---|
| Readonly field | Covariant | Can use subtype |
| Mutable field | Invariant | Must match exactly |
| Function parameter | Contravariant | Can use supertype |
| Function return | Covariant | Can use subtype |
Subtyping
Section titled “Subtyping”integeris a subtype ofnumberneveris a subtype of all types- All types are subtypes of
any - Union subtyping:
Ais subtype ofA | B
Gradual Adoption
Section titled “Gradual Adoption”Add types incrementally - untyped code continues to work:
-- Existing code works unchangedfunction old_function(x) return x + 1end
-- New code gets typesfunction new_function(x: number): number return x + 1endStart by adding types to:
- Function signatures at API boundaries
- HTTP handlers and queue consumers
- Critical business logic
Type Checking
Section titled “Type Checking”Run the type checker:
wippy lintReports type errors without executing code.