Typsystem
Wippy enthält ein graduelles Typsystem mit flusssensitiver Prüfung. Typen sind standardmäßig nicht-nullable.
Primitive
local n: number = 3.14
local i: integer = 42 -- integer ist Subtyp von number
local s: string = "hello"
local b: boolean = true
local a: any = "anything" -- explizit dynamisch (opt-out der Prüfung)
local u: unknown = something -- muss vor Verwendung eingeschränkt werden
any vs unknown
-- any: Opt-out aus der Typprüfung
local a: any = get_data()
a.foo.bar.baz() -- kein Fehler, kann zur Laufzeit abstürzen
-- unknown: sicheres Unbekannt, muss vor Verwendung eingeschränkt werden
local u: unknown = get_data()
u.foo -- FEHLER: kann auf Eigenschaft von unknown nicht zugreifen
if type(u) == "table" then
-- u hier auf table eingeschränkt
end
Nil-Sicherheit
Typen sind standardmäßig nicht-nullable. Verwenden Sie ? für optionale Werte:
local x: number = nil -- FEHLER: nil nicht an number zuweisbar
local y: number? = nil -- OK: number? bedeutet "number oder nil"
local z: number? = 42 -- OK
Kontrollfluss-Einschränkung
Der Typchecker verfolgt den Kontrollfluss:
local function process(x: number?): number
if x ~= nil then
return x -- x ist hier number
end
return 0
end
-- Early-Return-Muster
local user, err = get_user(123)
if err then return nil, err end
-- user hier auf non-nil eingeschränkt
-- Oder Standard
local val = get_value() or 0 -- val: number
Union-Typen
local val: number | string = get_value()
if type(val) == "number" then
print(val + 1) -- val: number
else
print(val:upper()) -- val: string
end
Literal-Typen
type Status = "pending" | "active" | "done"
local s: Status = "pending" -- OK
local s: Status = "invalid" -- FEHLER
Funktionstypen
local function add(a: number, b: number): number
return a + b
end
-- Mehrere Rückgaben
local function div_mod(a: number, b: number): (number, number)
return math.floor(a / b), a % b
end
-- Fehler-Rückgaben (Lua-Idiom)
local function fetch(url: string): (string?, error?)
-- gibt (data, nil) oder (nil, error) zurück
end
-- Erstklassige Funktionstypen
local double: (number) -> number = function(x: number): number
return x * 2
end
Variadische Funktionen
local function sum(...: number): number
local total: number = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
Record-Typen
type User = {name: string, age: number}
local u: User = {name = "alice", age = 25}
Optionale Felder
type Config = {
host: string,
port: number,
timeout?: number,
debug?: boolean
}
local cfg: Config = {host = "localhost", port = 8080} -- OK
Generics
local function identity<T>(x: T): T
return x
end
local n: number = identity(42)
local s: string = identity("hello")
Eingeschränkte Generics
type HasName = {name: string}
local function greet<T: HasName>(obj: T): string
return "Hello, " .. obj.name
end
greet({name = "Alice"}) -- OK
greet({age = 30}) -- FEHLER: fehlendes 'name'
Intersection-Typen
Kombinieren Sie mehrere Typen:
type Named = {name: string}
type Aged = {age: number}
type Person = Named & Aged
local p: Person = {name = "Alice", age = 30}
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
end
end
Der never-Typ
never ist der Bottom-Typ - es existieren keine Werte:
function fail(msg: string): never
error(msg)
end
Fehlerbehandlungsmuster
Der Checker versteht das Lua-Fehler-Idiom:
local value, err = call()
if err then
-- value ist hier nil
return nil, err
end
-- value ist hier non-nil, err ist nil
print(value)
Non-Nil-Assertion
Verwenden Sie ! um zu behaupten, dass ein Ausdruck non-nil ist:
local user: User? = get_user()
local name = user!.name -- behauptet, dass user non-nil ist
Wenn der Wert zur Laufzeit nil ist, wird ein Fehler ausgelöst. Verwenden Sie dies, wenn Sie wissen, dass ein Wert nicht nil sein kann, aber der Typchecker es nicht beweisen kann.
Typ-Casts
Sicherer Cast (Validierung)
Rufen Sie einen Typ als Funktion auf, um zu validieren und zu casten:
local data: any = get_json()
local user = User(data) -- validiert und gibt User zurück
local name = user.name -- sicherer Feldzugriff
Funktioniert mit Primitiven und benutzerdefinierten Typen:
local x: any = get_value()
local s = string(x) -- cast zu string
local n = integer(x) -- cast zu integer
local b = boolean(x) -- cast zu boolean
type Point = {x: number, y: number}
local p = Point(data) -- validiert Record-Struktur
Type:is()-Methode
Validieren ohne Fehler zu werfen, gibt (value, nil) oder (nil, error) zurück:
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 ist gültiger Point
else
return nil, err -- Validierung fehlgeschlagen
end
Das Ergebnis schränkt in Bedingungen ein:
if Point:is(data) then
local p: Point = data -- data auf Point eingeschränkt
end
Unsicherer Cast
Verwenden Sie :: oder as für ungeprüftes Casten:
local data: any = get_data()
local user = data :: User -- keine Laufzeitprüfung
local user = data as User -- gleich wie ::
Sparsam verwenden. Unsichere Casts umgehen die Validierung und können Laufzeitfehler verursachen, wenn der Wert nicht zum Typ passt.
Typ-Reflektion
Typen sind erstklassige Werte mit Introspektionsmethoden.
Art und Name
print(Number:kind()) -- "number"
print(Point:kind()) -- "record"
print(Point:name()) -- "Point"
Record-Felder
Iterieren Sie über Record-Felder:
type User = {name: string, age: number}
for name, typ in User:fields() do
print(name, typ:kind())
end
-- name string
-- age number
Greifen Sie auf einzelne Feldtypen zu:
local nameType = User.name -- Typ des 'name'-Feldes
print(nameType:kind()) -- "string"
Collection-Typen
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"
Optionale Typen
local opt: number? = nil
local optType = typeof(opt)
print(optType:kind()) -- "optional"
print(optType:inner():kind()) -- "number"
Union-Typen
type Status = "pending" | "active" | "done"
for variant in Status:variants() do
print(variant)
end
Funktionstypen
local fn: (number, string) -> boolean
local fnType = typeof(fn)
for param in fnType:params() do
print(param:kind())
end
print(fnType:ret():kind()) -- "boolean"
Typvergleich
print(Number == Number) -- true
print(Integer <= Number) -- true (Subtyp)
print(Integer < Number) -- true (strikter Subtyp)
Typen als Tabellenschlussel
local handlers = {}
handlers[Number] = function() return "number handler" end
handlers[String] = function() return "string handler" end
local h = handlers[typeof(value)]
if h then h() end
Typannotationen
Fügen Sie Typen zu Funktionssignaturen hinzu:
-- Parameter- und Rückgabetypen
local function process(input: string): number
return #input
end
-- Lokale Variablentypen
local count: number = 0
-- Typaliase
type StringArray = {string}
type StringMap = {[string]: number}
Typ-Validatoren
Fügen Sie Laufzeit-Validierungsbeschränkungen zu Typen mit Annotationen hinzu:
-- Einzelner Validator
local x: number @min(0) = 1
-- Mehrere Validatoren
local x: number @min(0) @max(100) = 50
-- String-Muster
local email: string @pattern("^.+@.+$") = "test@example.com"
-- Validator ohne Argument
local x: number @integer = 42
Eingebaute Validatoren
| Validator | Gilt für | Beispiel |
|---|---|---|
@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-Feld-Validatoren
type User = {
age: number @min(0) @max(150),
name: string @min_len(1) @max_len(100)
}
Array-Element-Validatoren
local scores: {number @min(0) @max(100)} = {85, 90}
Union-Member-Validatoren
local id: number @min(1) | string @min_len(1) = 1
Varianzregeln
| Position | Varianz | Beschreibung |
|---|---|---|
| Readonly-Feld | Kovariant | Kann Subtyp verwenden |
| Mutables Feld | Invariant | Muss exakt übereinstimmen |
| Funktionsparameter | Kontravariant | Kann Supertyp verwenden |
| Funktionsrückgabe | Kovariant | Kann Subtyp verwenden |
Subtyping
integerist Subtyp vonnumberneverist Subtyp aller Typen- Alle Typen sind Subtypen von
any - Union-Subtyping:
Aist Subtyp vonA | B
Graduelle Adoption
Fügen Sie Typen inkrementell hinzu - untypisierter Code funktioniert weiterhin:
-- Bestehender Code funktioniert unverändert
function old_function(x)
return x + 1
end
-- Neuer Code erhält Typen
function new_function(x: number): number
return x + 1
end
Beginnen Sie mit dem Hinzufügen von Typen zu:
- Funktionssignaturen an API-Grenzen
- HTTP-Handler und Queue-Consumer
- Kritische Geschäftslogik
Typprüfung
Führen Sie den Typchecker aus:
wippy lint
Meldet Typfehler ohne Code auszuführen.