타입 시스템
타입 시스템
섹션 제목: “타입 시스템”실험적. 일부 제약이 있을 수 있습니다.
Wippy는 흐름 민감(flow-sensitive) 검사를 갖춘 점진적 타입 시스템을 포함합니다. 타입은 기본적으로 nullable이 아닙니다.
프리미티브
섹션 제목: “프리미티브”local n: number = 3.14local i: integer = 42 -- integer는 number의 서브타입local s: string = "hello"local b: boolean = truelocal a: any = "anything" -- 명시적 동적 (검사 옵트아웃)local u: unknown = something -- 사용 전 좁혀야 함any와 unknown
섹션 제목: “any와 unknown”-- any: 타입 검사 옵트아웃local a: any = get_data()a.foo.bar.baz() -- 오류 없음, 런타임에 크래시할 수 있음
-- unknown: 안전한 unknown, 사용 전 좁혀야 함local u: unknown = get_data()u.foo -- 오류: unknown의 속성에 접근할 수 없음if type(u) == "table" then -- 여기서 u는 table로 좁혀짐endNil 안전성
섹션 제목: “Nil 안전성”타입은 기본적으로 nullable이 아닙니다. 선택적 값에는 ?를 사용합니다:
local x: number = nil -- 오류: nil은 number에 할당할 수 없음local y: number? = nil -- OK: number?는 "number 또는 nil"을 의미local z: number? = 42 -- OK제어 흐름 좁히기
섹션 제목: “제어 흐름 좁히기”타입 검사기는 제어 흐름을 추적합니다:
local function process(x: number?): number if x ~= nil then return x -- 여기서 x는 number end return 0end
-- 조기 반환 패턴local user, err = get_user(123)if err then return nil, err end-- 여기서 user는 nil이 아닌 것으로 좁혀짐
-- 또는 기본값local val = get_value() or 0 -- val: number유니온 타입
섹션 제목: “유니온 타입”local val: number | string = get_value()
if type(val) == "number" then print(val + 1) -- val: numberelse print(val:upper()) -- val: stringend리터럴 타입
섹션 제목: “리터럴 타입”type Status = "pending" | "active" | "done"
local s: Status = "pending" -- OKlocal s: Status = "invalid" -- 오류함수 타입
섹션 제목: “함수 타입”local function add(a: number, b: number): number return a + bend
-- 다중 반환local function div_mod(a: number, b: number): (number, number) return math.floor(a / b), a % bend
-- 오류 반환 (Lua 관용구)local function fetch(url: string): (string?, error?) -- (data, nil) 또는 (nil, error) 반환end
-- 1급 함수 타입local double: (number) -> number = function(x: number): number return x * 2end가변 인자 함수
섹션 제목: “가변 인자 함수”local function sum(...: number): number local total: number = 0 for _, v in ipairs({...}) do total = total + v end return totalend레코드 타입
섹션 제목: “레코드 타입”type User = {name: string, age: number}
local u: User = {name = "alice", age = 25}선택적 필드
섹션 제목: “선택적 필드”type Config = { host: string, port: number, timeout?: number, debug?: boolean}
local cfg: Config = {host = "localhost", port = 8080} -- OK제네릭
섹션 제목: “제네릭”local function identity<T>(x: T): T return xend
local n: number = identity(42)local s: string = identity("hello")제약된 제네릭
섹션 제목: “제약된 제네릭”type HasName = {name: string}
local function greet<T: HasName>(obj: T): string return "Hello, " .. obj.nameend
greet({name = "Alice"}) -- OKgreet({age = 30}) -- 오류: 'name' 누락인터섹션 타입
섹션 제목: “인터섹션 타입”여러 타입을 결합합니다:
type Named = {name: string}type Aged = {age: number}type Person = Named & Aged
local p: Person = {name = "Alice", age = 30}태그된 유니온
섹션 제목: “태그된 유니온”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 endendnever 타입
섹션 제목: “never 타입”never는 바텀 타입입니다 - 값이 존재하지 않습니다:
function fail(msg: string): never error(msg)end오류 처리 패턴
섹션 제목: “오류 처리 패턴”검사기는 Lua의 오류 관용구를 이해합니다:
local value, err = call()if err then -- 여기서 value는 nil return nil, errend-- 여기서 value는 nil이 아니고, err은 nilprint(value)Non-Nil 단언
섹션 제목: “Non-Nil 단언”!를 사용하여 표현식이 nil이 아님을 단언합니다:
local user: User? = get_user()local name = user!.name -- user가 nil이 아님을 단언런타임에 값이 nil이면 오류가 발생합니다. 값이 nil이 될 수 없다는 것을 알지만 타입 검사기가 증명할 수 없을 때 사용합니다.
타입 캐스트
섹션 제목: “타입 캐스트”안전한 캐스트 (검증)
섹션 제목: “안전한 캐스트 (검증)”타입을 함수로 호출하여 검증하고 캐스트합니다:
local data: any = get_json()local user = User(data) -- 검증하고 User 반환local name = user.name -- 안전한 필드 접근프리미티브와 커스텀 타입에서 작동합니다:
local x: any = get_value()local s = string(x) -- string으로 캐스트local n = integer(x) -- integer로 캐스트local b = boolean(x) -- boolean으로 캐스트
type Point = {x: number, y: number}local p = Point(data) -- 레코드 구조 검증Type:is() 메서드
섹션 제목: “Type:is() 메서드”예외를 던지지 않고 검증, (value, nil) 또는 (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는 유효한 Pointelse return nil, err -- 검증 실패end결과는 조건문에서 좁혀집니다:
if Point:is(data) then local p: Point = data -- data는 Point로 좁혀짐end안전하지 않은 캐스트
섹션 제목: “안전하지 않은 캐스트”체크되지 않은 캐스트에는 :: 또는 as를 사용합니다:
local data: any = get_data()local user = data :: User -- 런타임 검사 없음local user = data as User -- ::와 동일신중하게 사용하세요. 안전하지 않은 캐스트는 검증을 우회하며 값이 타입과 일치하지 않으면 런타임 오류를 일으킬 수 있습니다.
타입 리플렉션
섹션 제목: “타입 리플렉션”타입은 인트로스펙션 메서드가 있는 1급 값입니다.
종류와 이름
섹션 제목: “종류와 이름”print(Number:kind()) -- "number"print(Point:kind()) -- "record"print(Point:name()) -- "Point"레코드 필드
섹션 제목: “레코드 필드”레코드 필드를 순회합니다:
type User = {name: string, age: number}
for name, typ in User:fields() do print(name, typ:kind())end-- name string-- age number개별 필드 타입에 접근:
local nameType = User.name -- 'name' 필드의 타입print(nameType:kind()) -- "string"컬렉션 타입
섹션 제목: “컬렉션 타입”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"선택적 타입
섹션 제목: “선택적 타입”local opt: number? = nillocal optType = typeof(opt)print(optType:kind()) -- "optional"print(optType:inner():kind()) -- "number"유니온 타입
섹션 제목: “유니온 타입”type Status = "pending" | "active" | "done"
for variant in Status:variants() do print(variant)end함수 타입
섹션 제목: “함수 타입”local fn: (number, string) -> boolean
local fnType = typeof(fn)for param in fnType:params() do print(param:kind())endprint(fnType:ret():kind()) -- "boolean"타입 비교
섹션 제목: “타입 비교”print(Number == Number) -- trueprint(Integer <= Number) -- true (서브타입)print(Integer < Number) -- true (엄격한 서브타입)테이블 키로서의 타입
섹션 제목: “테이블 키로서의 타입”local handlers = {}handlers[Number] = function() return "number handler" endhandlers[String] = function() return "string handler" end
local h = handlers[typeof(value)]if h then h() end타입 어노테이션
섹션 제목: “타입 어노테이션”함수 시그니처에 타입을 추가합니다:
-- 파라미터와 반환 타입local function process(input: string): number return #inputend
-- 로컬 변수 타입local count: number = 0
-- 타입 별칭type StringArray = {string}type StringMap = {[string]: number}타입 검증자
섹션 제목: “타입 검증자”어노테이션을 사용하여 타입에 런타임 검증 제약을 추가합니다:
-- 단일 검증자local x: number @min(0) = 1
-- 여러 검증자local x: number @min(0) @max(100) = 50
-- 문자열 패턴local email: string @pattern("^.+@.+$") = "test@example.com"
-- 인자 없는 검증자local x: number @integer = 42내장 검증자
섹션 제목: “내장 검증자”| 검증자 | 적용 대상 | 예제 |
|---|---|---|
@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" |
레코드 필드 검증자
섹션 제목: “레코드 필드 검증자”type User = { age: number @min(0) @max(150), name: string @min_len(1) @max_len(100)}배열 요소 검증자
섹션 제목: “배열 요소 검증자”local scores: {number @min(0) @max(100)} = {85, 90}유니온 멤버 검증자
섹션 제목: “유니온 멤버 검증자”local id: number @min(1) | string @min_len(1) = 1변성(Variance) 규칙
섹션 제목: “변성(Variance) 규칙”| 위치 | 변성 | 설명 |
|---|---|---|
| 읽기 전용 필드 | 공변 | 서브타입 사용 가능 |
| 가변 필드 | 불변 | 정확히 일치해야 함 |
| 함수 파라미터 | 반공변 | 슈퍼타입 사용 가능 |
| 함수 반환 | 공변 | 서브타입 사용 가능 |
서브타이핑
섹션 제목: “서브타이핑”integer는number의 서브타입never는 모든 타입의 서브타입- 모든 타입은
any의 서브타입 - 유니온 서브타이핑:
A는A | B의 서브타입
점진적 도입
섹션 제목: “점진적 도입”타입을 점진적으로 추가합니다 - 타입이 없는 코드는 계속 작동합니다:
-- 기존 코드는 변경 없이 작동function old_function(x) return x + 1end
-- 새 코드는 타입을 가짐function new_function(x: number): number return x + 1end다음에 먼저 타입을 추가하기 시작하세요:
- API 경계의 함수 시그니처
- HTTP 핸들러 및 큐 컨슈머
- 핵심 비즈니스 로직
타입 검사
섹션 제목: “타입 검사”타입 검사기 실행:
wippy lint코드를 실행하지 않고 타입 오류를 보고합니다.