├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .luacheckrc ├── .luacov ├── LICENSE ├── README.md ├── default.project.json ├── lib ├── init.lua ├── init.spec.lua ├── t.d.ts └── ts.lua ├── package-lock.json ├── package.json ├── rotriever.toml ├── spec.lua ├── tsconfig.json └── wally.toml /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | with: 16 | submodules: recursive 17 | 18 | - name: Install Lua 19 | uses: leafo/gh-actions-lua@master 20 | with: 21 | luaVersion: 5.1 22 | 23 | - name: Install Luarocks 24 | uses: leafo/gh-actions-luarocks@master 25 | 26 | - name: Install Luarocks Packages 27 | run: | 28 | luarocks install busted 29 | luarocks install luacheck 30 | luarocks install luacov 31 | luarocks install luacov-reporter-lcov 32 | luarocks install luafilesystem 33 | luarocks install md5 34 | 35 | - name: Verify LuaCheck 36 | run: luacheck lib 37 | 38 | - name: Run Tests 39 | run: | 40 | lua -lluacov spec.lua 41 | luacov -r lcov 42 | 43 | - name: Coveralls 44 | uses: coverallsapp/github-action@master 45 | with: 46 | github-token: ${{ secrets.github_token }} 47 | path-to-lcov: ./luacov.report.out 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "modules/lemur"] 2 | path = modules/lemur 3 | url = https://github.com/LPGhatguy/lemur 4 | [submodule "modules/testez"] 5 | path = modules/testez 6 | url = https://github.com/Roblox/testez 7 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 2 | globals = { 3 | -- global variables 4 | "game", "script", 5 | 6 | -- global functions 7 | "delay", "getfenv", "setfenv", "settings", "spawn", "tick", "time", 8 | "typeof", "unpack", "UserSettings", "wait", "warn", "version", 9 | 10 | -- types 11 | "Axes", "BrickColor", "CFrame", "Color3", "ColorSequence", "ColorSequenceKeypoint", 12 | "Enum", "Faces", "Instance", "NumberRange", "NumberSequence", "NumberSequenceKeypoint", 13 | "PhysicalProperties", "Random", "Ray", "Rect", "Region3", "Region3int16", "TweenInfo", 14 | "UDim", "UDim2", "Vector2", "Vector3", "Vector3int16", 15 | 16 | -- math library 17 | "math.clamp", "math.noise", "math.sign", 18 | 19 | -- debug library 20 | "debug.profilebegin", "debug.profileend", 21 | 22 | "it", "expect", "describe", 23 | } 24 | 25 | -- fix methods 26 | ignore = {"self", "super"} 27 | 28 | -- prevent max line lengths 29 | max_line_length = false 30 | max_code_line_length = false 31 | max_string_line_length = false 32 | max_comment_line_length = false -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return { 2 | include = { 3 | "^lib", 4 | }, 5 | exclude = { 6 | "%.spec$", 7 | }, 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Osyris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

t

2 |
3 | 4 | CI Status 5 | 6 | 7 | Coverage Status 8 | 9 |
10 | 11 |
12 | A Runtime Type Checker for Roblox 13 |
14 | 15 |
 
16 | 17 | t is a module which allows you to create type definitions to check values against. 18 | 19 | ## Download 20 | [You can download the latest copy of t here.](https://raw.githubusercontent.com/osyrisrblx/t/master/lib/init.lua) 21 | 22 | ## Why? 23 | When building large systems, it can often be difficult to find type mismatch bugs.\ 24 | Typechecking helps you ensure that your functions are recieving the appropriate types for their arguments. 25 | 26 | In Roblox specifically, it is important to type check your Remote objects to ensure that exploiters aren't sending you bad data which can cause your server to error (and potentially crash!). 27 | 28 | ## Crash Course 29 | ```Lua 30 | local t = require(path.to.t) 31 | 32 | local fooCheck = t.tuple(t.string, t.number, t.optional(t.string)) 33 | local function foo(a, b, c) 34 | assert(fooCheck(a, b, c)) 35 | -- you can now assume: 36 | -- a is a string 37 | -- b is a number 38 | -- c is either a string or nil 39 | end 40 | 41 | foo() --> Error: Bad tuple index #1: string expected, got nil 42 | foo("1", 2) 43 | foo("1", 2, "3") 44 | foo("1", 2, 3) --> Error: Bad tuple index #3: (optional) string expected, got number 45 | ``` 46 | 47 | Check out src/t.spec.lua for a variety of good examples! 48 | 49 | ## Primitives 50 | |Type | |Member | 51 | |---------|--|-----------| 52 | |boolean |=>|t.boolean | 53 | |thread |=>|t.thread | 54 | |function |=>|t.callback | 55 | |nil |=>|t.none | 56 | |number |=>|t.number | 57 | |string |=>|t.string | 58 | |table |=>|t.table | 59 | |userdata |=>|t.userdata | 60 | 61 | Any primitive can be checked with a built-in primitive function.\ 62 | Primitives are found under the same name as their type name except for two: 63 | - nil -> t.none 64 | - function -> t.callback 65 | 66 | These two are renamed due to Lua restrictions on reserved words. 67 | 68 | All Roblox primitives are also available and can be found under their respective type names.\ 69 | We won't list them here to due how many there are, but as an example you can access a few like this: 70 | ```Lua 71 | t.Instance 72 | t.CFrame 73 | t.Color3 74 | t.Vector3 75 | -- etc... 76 | ``` 77 | 78 | You can check values against these primitives like this: 79 | ```Lua 80 | local x = 1 81 | print(t.number(x)) --> true 82 | print(t.string(x)) --> false, "string expected, got number" 83 | ``` 84 | 85 | ## Type Composition 86 | Often, you can combine types to create a composition of types.\ 87 | For example: 88 | ```Lua 89 | local mightBeAString = t.optional(t.string) 90 | print(mightBeAString("Hello")) --> true 91 | print(mightBeAString()) --> true 92 | print(mightBeAString(1)) --> false, "(optional) string expected, got number" 93 | ``` 94 | 95 | These get denoted as function calls below with specified arguments. `check` can be any other type checker. 96 | 97 | ## Meta Type Functions 98 | The real power of t is in the meta type functions. 99 | 100 | **`t.any`**\ 101 | Passes if value is non-nil. 102 | 103 | **`t.literal(...)`**\ 104 | Passes if value matches any given value exactly. 105 | 106 | **`t.keyOf(keyTable)`**\ 107 | Returns a t.union of each key in the table as a t.literal 108 | 109 | **`t.valueOf(valueTable)`**\ 110 | Returns a t.union of each value in the table as a t.literal 111 | 112 | **`t.optional(check)`**\ 113 | Passes if value is either nil or passes `check` 114 | 115 | **`t.tuple(...)`**\ 116 | You can define a tuple type with `t.tuple(...)`.\ 117 | The arguments should be a list of type checkers. 118 | 119 | **`t.union(...)`** - ( alias: `t.some(...)` )\ 120 | You can define a union type with `t.union(...)`.\ 121 | The arguments should be a list of type checkers.\ 122 | **At least one check must pass**\ 123 | i.e. `t.union(a, b, c)` -> `a OR b OR c` 124 | 125 | **`t.intersection(...)`** - ( alias: `t.every(...)` )\ 126 | You can define an intersection type with `t.intersection(...)`.\ 127 | The arguments should be a list of type checkers.\ 128 | **All checks must pass**\ 129 | i.e. `t.intersection(a, b, c)` -> `a AND b AND c` 130 | 131 | **`t.keys(check)`**\ 132 | Matches a table's keys against `check` 133 | 134 | **`t.values(check)`**\ 135 | Matches a table's values against `check` 136 | 137 | **`t.map(keyCheck, valueCheck)`**\ 138 | Checks all of a table's keys against `keyCheck` and all of a table's values against `valueCheck` 139 | 140 | There's also type checks for arrays and interfaces but we'll cover those in their own sections! 141 | 142 | ## Special Number Functions 143 | 144 | t includes a few special functions for checking numbers, these can be useful to ensure the given value is within a certain range. 145 | 146 | **General:**\ 147 | **`t.nan`**\ 148 | determines if value is `NaN`\ 149 | All of the following checks will not pass for `NaN` values.\ 150 | If you need to allow for `NaN`, use `t.union(t.number, t.nan)` 151 | 152 | **`t.integer`**\ 153 | checks `t.number` and determines if value is an integer 154 | 155 | **`t.numberPositive`**\ 156 | checks `t.number` and determines if the value > 0 157 | 158 | **`t.numberNegative`**\ 159 | checks `t.number` and determines if the value < 0 160 | 161 | **Inclusive Comparisons:**\ 162 | **`t.numberMin(min)`**\ 163 | checks `t.number` and determines if value >= min 164 | 165 | **`t.numberMax(max)`**\ 166 | checks `t.number` and determines if value <= max 167 | 168 | **`t.numberConstrained(min, max)`**\ 169 | checks `t.number` and determines if min <= value <= max 170 | 171 | **Exclusive Comparisons:**\ 172 | **`t.numberMinExclusive(min)`**\ 173 | checks `t.number` and determines if value > min 174 | 175 | **`t.numberMaxExclusive(max)`**\ 176 | checks `t.number` and determines if value < max 177 | 178 | **`t.numberConstrainedExclusive(min, max)`**\ 179 | checks `t.number` and determines if min < value < max 180 | 181 | ## Special String Functions 182 | 183 | t includes a few special functions for checking strings 184 | 185 | **`t.match(pattern)`**\ 186 | checks `t.string` and determines if value matches the pattern via `string.match(value, pattern)` 187 | 188 | ## Arrays 189 | In Lua, arrays are a special type of table where all the keys are sequential integers.\ 190 | t has special functions for checking against arrays. 191 | 192 | **`t.array(check)`**\ 193 | determines that the value is a table and all of it's keys are sequential integers and ensures all of the values in the table match `check` 194 | 195 | ## Interfaces 196 | Interfaces can be defined through `t.interface(definition)` where `definition` is a table of type checkers.\ 197 | For example: 198 | ```Lua 199 | local IPlayer = t.interface({ 200 | Name = t.string, 201 | Score = t.number, 202 | }) 203 | 204 | local myPlayer = { Name = "TestPlayer", Score = 100 } 205 | print(IPlayer(myPlayer)) --> true 206 | print(IPlayer({})) --> false, "[interface] bad value for Name: string expected, got nil" 207 | ``` 208 | 209 | You can use `t.optional(check)` to make an interface field optional or `t.union(...)` if a field can be multiple types. 210 | 211 | You can even put interfaces inside interfaces! 212 | ```Lua 213 | local IPlayer = t.interface({ 214 | Name = t.string, 215 | Score = t.number, 216 | Inventory = t.interface({ 217 | Size = t.number 218 | }) 219 | }) 220 | 221 | local myPlayer = { 222 | Name = "TestPlayer", 223 | Score = 100, 224 | Inventory = { 225 | Size = 20 226 | } 227 | } 228 | print(IPlayer(myPlayer)) --> true 229 | ``` 230 | 231 | If you want to make sure an value _exactly_ matches a given interface (no extra fields),\ 232 | you can use `t.strictInterface(definition)` where `definition` is a table of type checkers.\ 233 | For example: 234 | ```Lua 235 | local IPlayer = t.strictInterface({ 236 | Name = t.string, 237 | Score = t.number, 238 | }) 239 | 240 | local myPlayer1 = { Name = "TestPlayer", Score = 100 } 241 | local myPlayer2 = { Name = "TestPlayer", Score = 100, A = 1 } 242 | print(IPlayer(myPlayer1)) --> true 243 | print(IPlayer(myPlayer2)) --> false, "[interface] unexpected field 'A'" 244 | ``` 245 | 246 | ## Roblox Instances 247 | t includes two functions to check the types of Roblox Instances. 248 | 249 | **`t.instanceOf(className[, childTable])`**\ 250 | ensures the value is an Instance and it's ClassName exactly matches `className`\ 251 | If you provide a `childTable`, it will be automatically passed to `t.children()` 252 | 253 | **`t.instanceIsA(className[, childTable])`**\ 254 | ensures the value is an Instance and it's ClassName matches `className` by a IsA comparison. ([see here](http://wiki.roblox.com/index.php?title=API:Class/Instance/FindFirstAncestorWhichIsA)) 255 | 256 | **`t.children(checkTable)`**\ 257 | Takes a table where keys are child names and values are functions to check the children against.\ 258 | Pass an instance tree into the function. 259 | 260 | **Warning! If you pass in a tree with more than one child of the same name, this function will always return false** 261 | 262 | ## Roblox Enums 263 | 264 | t allows type checking for Roblox Enums! 265 | 266 | **`t.Enum`**\ 267 | Ensures the value is an Enum, i.e. `Enum.Material`. 268 | 269 | **`t.EnumItem`**\ 270 | Ensures the value is an EnumItem, i.e. `Enum.Material.Plastic`. 271 | 272 | but the real power here is: 273 | 274 | **`t.enum(enum)`**\ 275 | This will pass if value is an EnumItem which belongs to `enum`. 276 | 277 | ## Function Wrapping 278 | Here's a common pattern people use when working with t: 279 | ```Lua 280 | local fooCheck = t.tuple(t.string, t.number, t.optional(t.string)) 281 | local function foo(a, b, c) 282 | assert(fooCheck(a, b, c)) 283 | -- function now assumes a, b, c are valid 284 | end 285 | ``` 286 | 287 | **`t.wrap(callback, argCheck)`**\ 288 | `t.wrap(callback, argCheck)` allows you to shorten this to the following: 289 | ```Lua 290 | local fooCheck = t.tuple(t.string, t.number, t.optional(t.string)) 291 | local foo = t.wrap(function(a, b, c) 292 | -- function now assumes a, b, c are valid 293 | end, fooCheck) 294 | ``` 295 | 296 | OR 297 | 298 | ```Lua 299 | local foo = t.wrap(function(a, b, c) 300 | -- function now assumes a, b, c are valid 301 | end, t.tuple(t.string, t.number, t.optional(t.string))) 302 | ``` 303 | 304 | Alternatively, there's also: 305 | **`t.strict(check)`**\ 306 | wrap your whole type in `t.strict(check)` and it will run an `assert` on calls.\ 307 | The example from above could alternatively look like: 308 | ```Lua 309 | local fooCheck = t.strict(t.tuple(t.string, t.number, t.optional(t.string))) 310 | local function foo(a, b, c) 311 | fooCheck(a, b, c) 312 | -- function now assumes a, b, c are valid 313 | end 314 | ``` 315 | 316 | ## Tips and Tricks 317 | You can create your own type checkers with a simple function that returns a boolean.\ 318 | These custom type checkers fit perfectly with the rest of t's functions. 319 | 320 | If you roll your own custom OOP framework, you can easily integrate t with a custom type checker.\ 321 | For example: 322 | ```Lua 323 | local MyClass = {} 324 | MyClass.__index = MyClass 325 | 326 | function MyClass.new() 327 | local self = setmetatable({}, MyClass) 328 | -- setup instance 329 | return self 330 | end 331 | 332 | local function instanceOfClass(class) 333 | return function(value) 334 | local tableSuccess, tableErrMsg = t.table(value) 335 | if not tableSuccess then 336 | return false, tableErrMsg or "" -- pass error message for value not being a table 337 | end 338 | 339 | local mt = getmetatable(value) 340 | if not mt or mt.__index ~= class then 341 | return false, "bad member of class" -- custom error message 342 | end 343 | 344 | return true -- all checks passed 345 | end 346 | end 347 | 348 | local instanceOfMyClass = instanceOfClass(MyClass) 349 | 350 | local myObject = MyClass.new() 351 | print(instanceOfMyClass(myObject)) --> true 352 | ``` 353 | 354 | ## Known Issues 355 | 356 | You can put a `t.tuple(...)` inside an array or interface, but that doesn't really make any sense..\ 357 | In the future, this may error. 358 | 359 | ## Notes 360 | This library was heavily inspired by [io-ts](https://github.com/gcanti/io-ts), a fantastic runtime type validation library for TypeScript. 361 | 362 | ## Why did you name it t? 363 | The whole idea is that most people import modules via:\ 364 | `local X = require(path.to.X)`\ 365 | So whatever I name the library will be what people name the variable.\ 366 | If I made the name of the library longer, the type definitions become more noisy / less readable.\ 367 | Things like this are pretty common:\ 368 | `local fooCheck = t.tuple(t.string, t.number, t.optional(t.string))` 369 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t", 3 | "tree": { 4 | "$path": "lib" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/init.lua: -------------------------------------------------------------------------------- 1 | -- t: a runtime typechecker for Roblox 2 | 3 | local t = {} 4 | 5 | function t.type(typeName) 6 | return function(value) 7 | local valueType = type(value) 8 | if valueType == typeName then 9 | return true 10 | else 11 | return false, string.format("%s expected, got %s", typeName, valueType) 12 | end 13 | end 14 | end 15 | 16 | function t.typeof(typeName) 17 | return function(value) 18 | local valueType = typeof(value) 19 | if valueType == typeName then 20 | return true 21 | else 22 | return false, string.format("%s expected, got %s", typeName, valueType) 23 | end 24 | end 25 | end 26 | 27 | --[[** 28 | matches any type except nil 29 | 30 | @param value The value to check against 31 | 32 | @returns True iff the condition is satisfied, false otherwise 33 | **--]] 34 | function t.any(value) 35 | if value ~= nil then 36 | return true 37 | else 38 | return false, "any expected, got nil" 39 | end 40 | end 41 | 42 | --Lua primitives 43 | 44 | --[[** 45 | ensures Lua primitive boolean type 46 | 47 | @param value The value to check against 48 | 49 | @returns True iff the condition is satisfied, false otherwise 50 | **--]] 51 | t.boolean = t.typeof("boolean") 52 | 53 | --[[** 54 | ensures Lua primitive thread type 55 | 56 | @param value The value to check against 57 | 58 | @returns True iff the condition is satisfied, false otherwise 59 | **--]] 60 | t.thread = t.typeof("thread") 61 | 62 | --[[** 63 | ensures Lua primitive callback type 64 | 65 | @param value The value to check against 66 | 67 | @returns True iff the condition is satisfied, false otherwise 68 | **--]] 69 | t.callback = t.typeof("function") 70 | t["function"] = t.callback 71 | 72 | --[[** 73 | ensures Lua primitive none type 74 | 75 | @param value The value to check against 76 | 77 | @returns True iff the condition is satisfied, false otherwise 78 | **--]] 79 | t.none = t.typeof("nil") 80 | t["nil"] = t.none 81 | 82 | --[[** 83 | ensures Lua primitive string type 84 | 85 | @param value The value to check against 86 | 87 | @returns True iff the condition is satisfied, false otherwise 88 | **--]] 89 | t.string = t.typeof("string") 90 | 91 | --[[** 92 | ensures Lua primitive table type 93 | 94 | @param value The value to check against 95 | 96 | @returns True iff the condition is satisfied, false otherwise 97 | **--]] 98 | t.table = t.typeof("table") 99 | 100 | --[[** 101 | ensures Lua primitive userdata type 102 | 103 | @param value The value to check against 104 | 105 | @returns True iff the condition is satisfied, false otherwise 106 | **--]] 107 | t.userdata = t.type("userdata") 108 | 109 | --[[** 110 | ensures value is a number and non-NaN 111 | 112 | @param value The value to check against 113 | 114 | @returns True iff the condition is satisfied, false otherwise 115 | **--]] 116 | function t.number(value) 117 | local valueType = typeof(value) 118 | if valueType == "number" then 119 | if value == value then 120 | return true 121 | else 122 | return false, "unexpected NaN value" 123 | end 124 | else 125 | return false, string.format("number expected, got %s", valueType) 126 | end 127 | end 128 | 129 | --[[** 130 | ensures value is NaN 131 | 132 | @param value The value to check against 133 | 134 | @returns True iff the condition is satisfied, false otherwise 135 | **--]] 136 | function t.nan(value) 137 | local valueType = typeof(value) 138 | if valueType == "number" then 139 | if value ~= value then 140 | return true 141 | else 142 | return false, "unexpected non-NaN value" 143 | end 144 | else 145 | return false, string.format("number expected, got %s", valueType) 146 | end 147 | end 148 | 149 | -- roblox types 150 | 151 | --[[** 152 | ensures Roblox Axes type 153 | 154 | @param value The value to check against 155 | 156 | @returns True iff the condition is satisfied, false otherwise 157 | **--]] 158 | t.Axes = t.typeof("Axes") 159 | 160 | --[[** 161 | ensures Roblox BrickColor type 162 | 163 | @param value The value to check against 164 | 165 | @returns True iff the condition is satisfied, false otherwise 166 | **--]] 167 | t.BrickColor = t.typeof("BrickColor") 168 | 169 | --[[** 170 | ensures Roblox CatalogSearchParams type 171 | 172 | @param value The value to check against 173 | 174 | @returns True iff the condition is satisfied, false otherwise 175 | **--]] 176 | t.CatalogSearchParams = t.typeof("CatalogSearchParams") 177 | 178 | --[[** 179 | ensures Roblox CFrame type 180 | 181 | @param value The value to check against 182 | 183 | @returns True iff the condition is satisfied, false otherwise 184 | **--]] 185 | t.CFrame = t.typeof("CFrame") 186 | 187 | --[[** 188 | ensures Roblox Color3 type 189 | 190 | @param value The value to check against 191 | 192 | @returns True iff the condition is satisfied, false otherwise 193 | **--]] 194 | t.Color3 = t.typeof("Color3") 195 | 196 | --[[** 197 | ensures Roblox ColorSequence type 198 | 199 | @param value The value to check against 200 | 201 | @returns True iff the condition is satisfied, false otherwise 202 | **--]] 203 | t.ColorSequence = t.typeof("ColorSequence") 204 | 205 | --[[** 206 | ensures Roblox ColorSequenceKeypoint type 207 | 208 | @param value The value to check against 209 | 210 | @returns True iff the condition is satisfied, false otherwise 211 | **--]] 212 | t.ColorSequenceKeypoint = t.typeof("ColorSequenceKeypoint") 213 | 214 | --[[** 215 | ensures Roblox DateTime type 216 | 217 | @param value The value to check against 218 | 219 | @returns True iff the condition is satisfied, false otherwise 220 | **--]] 221 | t.DateTime = t.typeof("DateTime") 222 | 223 | --[[** 224 | ensures Roblox DockWidgetPluginGuiInfo type 225 | 226 | @param value The value to check against 227 | 228 | @returns True iff the condition is satisfied, false otherwise 229 | **--]] 230 | t.DockWidgetPluginGuiInfo = t.typeof("DockWidgetPluginGuiInfo") 231 | 232 | --[[** 233 | ensures Roblox Enum type 234 | 235 | @param value The value to check against 236 | 237 | @returns True iff the condition is satisfied, false otherwise 238 | **--]] 239 | t.Enum = t.typeof("Enum") 240 | 241 | --[[** 242 | ensures Roblox EnumItem type 243 | 244 | @param value The value to check against 245 | 246 | @returns True iff the condition is satisfied, false otherwise 247 | **--]] 248 | t.EnumItem = t.typeof("EnumItem") 249 | 250 | --[[** 251 | ensures Roblox Enums type 252 | 253 | @param value The value to check against 254 | 255 | @returns True iff the condition is satisfied, false otherwise 256 | **--]] 257 | t.Enums = t.typeof("Enums") 258 | 259 | --[[** 260 | ensures Roblox Faces type 261 | 262 | @param value The value to check against 263 | 264 | @returns True iff the condition is satisfied, false otherwise 265 | **--]] 266 | t.Faces = t.typeof("Faces") 267 | 268 | --[[** 269 | ensures Roblox FloatCurveKey type 270 | 271 | @param value The value to check against 272 | 273 | @returns True iff the condition is satisfied, false otherwise 274 | **--]] 275 | t.FloatCurveKey = t.typeof("FloatCurveKey") 276 | 277 | --[[** 278 | ensures Roblox Font type 279 | 280 | @param value The value to check against 281 | 282 | @returns True iff the condition is satisfied, false otherwise 283 | **--]] 284 | t.Font = t.typeof("Font") 285 | 286 | --[[** 287 | ensures Roblox Instance type 288 | 289 | @param value The value to check against 290 | 291 | @returns True iff the condition is satisfied, false otherwise 292 | **--]] 293 | t.Instance = t.typeof("Instance") 294 | 295 | --[[** 296 | ensures Roblox NumberRange type 297 | 298 | @param value The value to check against 299 | 300 | @returns True iff the condition is satisfied, false otherwise 301 | **--]] 302 | t.NumberRange = t.typeof("NumberRange") 303 | 304 | --[[** 305 | ensures Roblox NumberSequence type 306 | 307 | @param value The value to check against 308 | 309 | @returns True iff the condition is satisfied, false otherwise 310 | **--]] 311 | t.NumberSequence = t.typeof("NumberSequence") 312 | 313 | --[[** 314 | ensures Roblox NumberSequenceKeypoint type 315 | 316 | @param value The value to check against 317 | 318 | @returns True iff the condition is satisfied, false otherwise 319 | **--]] 320 | t.NumberSequenceKeypoint = t.typeof("NumberSequenceKeypoint") 321 | 322 | --[[** 323 | ensures Roblox OverlapParams type 324 | 325 | @param value The value to check against 326 | 327 | @returns True iff the condition is satisfied, false otherwise 328 | **--]] 329 | t.OverlapParams = t.typeof("OverlapParams") 330 | 331 | --[[** 332 | ensures Roblox PathWaypoint type 333 | 334 | @param value The value to check against 335 | 336 | @returns True iff the condition is satisfied, false otherwise 337 | **--]] 338 | t.PathWaypoint = t.typeof("PathWaypoint") 339 | 340 | --[[** 341 | ensures Roblox PhysicalProperties type 342 | 343 | @param value The value to check against 344 | 345 | @returns True iff the condition is satisfied, false otherwise 346 | **--]] 347 | t.PhysicalProperties = t.typeof("PhysicalProperties") 348 | 349 | --[[** 350 | ensures Roblox Random type 351 | 352 | @param value The value to check against 353 | 354 | @returns True iff the condition is satisfied, false otherwise 355 | **--]] 356 | t.Random = t.typeof("Random") 357 | 358 | --[[** 359 | ensures Roblox Ray type 360 | 361 | @param value The value to check against 362 | 363 | @returns True iff the condition is satisfied, false otherwise 364 | **--]] 365 | t.Ray = t.typeof("Ray") 366 | 367 | --[[** 368 | ensures Roblox RaycastParams type 369 | 370 | @param value The value to check against 371 | 372 | @returns True iff the condition is satisfied, false otherwise 373 | **--]] 374 | t.RaycastParams = t.typeof("RaycastParams") 375 | 376 | --[[** 377 | ensures Roblox RaycastResult type 378 | 379 | @param value The value to check against 380 | 381 | @returns True iff the condition is satisfied, false otherwise 382 | **--]] 383 | t.RaycastResult = t.typeof("RaycastResult") 384 | 385 | --[[** 386 | ensures Roblox RBXScriptConnection type 387 | 388 | @param value The value to check against 389 | 390 | @returns True iff the condition is satisfied, false otherwise 391 | **--]] 392 | t.RBXScriptConnection = t.typeof("RBXScriptConnection") 393 | 394 | --[[** 395 | ensures Roblox RBXScriptSignal type 396 | 397 | @param value The value to check against 398 | 399 | @returns True iff the condition is satisfied, false otherwise 400 | **--]] 401 | t.RBXScriptSignal = t.typeof("RBXScriptSignal") 402 | 403 | --[[** 404 | ensures Roblox Rect type 405 | 406 | @param value The value to check against 407 | 408 | @returns True iff the condition is satisfied, false otherwise 409 | **--]] 410 | t.Rect = t.typeof("Rect") 411 | 412 | --[[** 413 | ensures Roblox Region3 type 414 | 415 | @param value The value to check against 416 | 417 | @returns True iff the condition is satisfied, false otherwise 418 | **--]] 419 | t.Region3 = t.typeof("Region3") 420 | 421 | --[[** 422 | ensures Roblox Region3int16 type 423 | 424 | @param value The value to check against 425 | 426 | @returns True iff the condition is satisfied, false otherwise 427 | **--]] 428 | t.Region3int16 = t.typeof("Region3int16") 429 | 430 | --[[** 431 | ensures Roblox TweenInfo type 432 | 433 | @param value The value to check against 434 | 435 | @returns True iff the condition is satisfied, false otherwise 436 | **--]] 437 | t.TweenInfo = t.typeof("TweenInfo") 438 | 439 | --[[** 440 | ensures Roblox UDim type 441 | 442 | @param value The value to check against 443 | 444 | @returns True iff the condition is satisfied, false otherwise 445 | **--]] 446 | t.UDim = t.typeof("UDim") 447 | 448 | --[[** 449 | ensures Roblox UDim2 type 450 | 451 | @param value The value to check against 452 | 453 | @returns True iff the condition is satisfied, false otherwise 454 | **--]] 455 | t.UDim2 = t.typeof("UDim2") 456 | 457 | --[[** 458 | ensures Roblox Vector2 type 459 | 460 | @param value The value to check against 461 | 462 | @returns True iff the condition is satisfied, false otherwise 463 | **--]] 464 | t.Vector2 = t.typeof("Vector2") 465 | 466 | --[[** 467 | ensures Roblox Vector2int16 type 468 | 469 | @param value The value to check against 470 | 471 | @returns True iff the condition is satisfied, false otherwise 472 | **--]] 473 | t.Vector2int16 = t.typeof("Vector2int16") 474 | 475 | --[[** 476 | ensures Roblox Vector3 type 477 | 478 | @param value The value to check against 479 | 480 | @returns True iff the condition is satisfied, false otherwise 481 | **--]] 482 | t.Vector3 = t.typeof("Vector3") 483 | 484 | --[[** 485 | ensures Roblox Vector3int16 type 486 | 487 | @param value The value to check against 488 | 489 | @returns True iff the condition is satisfied, false otherwise 490 | **--]] 491 | t.Vector3int16 = t.typeof("Vector3int16") 492 | 493 | --[[** 494 | ensures value is a given literal value 495 | 496 | @param literal The literal to use 497 | 498 | @returns A function that will return true iff the condition is passed 499 | **--]] 500 | function t.literal(...) 501 | local size = select("#", ...) 502 | if size == 1 then 503 | local literal = ... 504 | return function(value) 505 | if value ~= literal then 506 | return false, string.format("expected %s, got %s", tostring(literal), tostring(value)) 507 | end 508 | 509 | return true 510 | end 511 | else 512 | local literals = {} 513 | for i = 1, size do 514 | local value = select(i, ...) 515 | literals[i] = t.literal(value) 516 | end 517 | 518 | return t.union(table.unpack(literals, 1, size)) 519 | end 520 | end 521 | 522 | --[[** 523 | DEPRECATED 524 | Please use t.literal 525 | **--]] 526 | t.exactly = t.literal 527 | 528 | --[[** 529 | Returns a t.union of each key in the table as a t.literal 530 | 531 | @param keyTable The table to get keys from 532 | 533 | @returns True iff the condition is satisfied, false otherwise 534 | **--]] 535 | function t.keyOf(keyTable) 536 | local keys = {} 537 | local length = 0 538 | for key in pairs(keyTable) do 539 | length = length + 1 540 | keys[length] = key 541 | end 542 | 543 | return t.literal(table.unpack(keys, 1, length)) 544 | end 545 | 546 | --[[** 547 | Returns a t.union of each value in the table as a t.literal 548 | 549 | @param valueTable The table to get values from 550 | 551 | @returns True iff the condition is satisfied, false otherwise 552 | **--]] 553 | function t.valueOf(valueTable) 554 | local values = {} 555 | local length = 0 556 | for _, value in pairs(valueTable) do 557 | length = length + 1 558 | values[length] = value 559 | end 560 | 561 | return t.literal(table.unpack(values, 1, length)) 562 | end 563 | 564 | --[[** 565 | ensures value is an integer 566 | 567 | @param value The value to check against 568 | 569 | @returns True iff the condition is satisfied, false otherwise 570 | **--]] 571 | function t.integer(value) 572 | local success, errMsg = t.number(value) 573 | if not success then 574 | return false, errMsg or "" 575 | end 576 | 577 | if value % 1 == 0 then 578 | return true 579 | else 580 | return false, string.format("integer expected, got %s", value) 581 | end 582 | end 583 | 584 | --[[** 585 | ensures value is a number where min <= value 586 | 587 | @param min The minimum to use 588 | 589 | @returns A function that will return true iff the condition is passed 590 | **--]] 591 | function t.numberMin(min) 592 | return function(value) 593 | local success, errMsg = t.number(value) 594 | if not success then 595 | return false, errMsg or "" 596 | end 597 | 598 | if value >= min then 599 | return true 600 | else 601 | return false, string.format("number >= %s expected, got %s", min, value) 602 | end 603 | end 604 | end 605 | 606 | --[[** 607 | ensures value is a number where value <= max 608 | 609 | @param max The maximum to use 610 | 611 | @returns A function that will return true iff the condition is passed 612 | **--]] 613 | function t.numberMax(max) 614 | return function(value) 615 | local success, errMsg = t.number(value) 616 | if not success then 617 | return false, errMsg 618 | end 619 | 620 | if value <= max then 621 | return true 622 | else 623 | return false, string.format("number <= %s expected, got %s", max, value) 624 | end 625 | end 626 | end 627 | 628 | --[[** 629 | ensures value is a number where min < value 630 | 631 | @param min The minimum to use 632 | 633 | @returns A function that will return true iff the condition is passed 634 | **--]] 635 | function t.numberMinExclusive(min) 636 | return function(value) 637 | local success, errMsg = t.number(value) 638 | if not success then 639 | return false, errMsg or "" 640 | end 641 | 642 | if min < value then 643 | return true 644 | else 645 | return false, string.format("number > %s expected, got %s", min, value) 646 | end 647 | end 648 | end 649 | 650 | --[[** 651 | ensures value is a number where value < max 652 | 653 | @param max The maximum to use 654 | 655 | @returns A function that will return true iff the condition is passed 656 | **--]] 657 | function t.numberMaxExclusive(max) 658 | return function(value) 659 | local success, errMsg = t.number(value) 660 | if not success then 661 | return false, errMsg or "" 662 | end 663 | 664 | if value < max then 665 | return true 666 | else 667 | return false, string.format("number < %s expected, got %s", max, value) 668 | end 669 | end 670 | end 671 | 672 | --[[** 673 | ensures value is a number where value > 0 674 | 675 | @returns A function that will return true iff the condition is passed 676 | **--]] 677 | t.numberPositive = t.numberMinExclusive(0) 678 | 679 | --[[** 680 | ensures value is a number where value < 0 681 | 682 | @returns A function that will return true iff the condition is passed 683 | **--]] 684 | t.numberNegative = t.numberMaxExclusive(0) 685 | 686 | --[[** 687 | ensures value is a number where min <= value <= max 688 | 689 | @param min The minimum to use 690 | @param max The maximum to use 691 | 692 | @returns A function that will return true iff the condition is passed 693 | **--]] 694 | function t.numberConstrained(min, max) 695 | assert(t.number(min)) 696 | assert(t.number(max)) 697 | local minCheck = t.numberMin(min) 698 | local maxCheck = t.numberMax(max) 699 | 700 | return function(value) 701 | local minSuccess, minErrMsg = minCheck(value) 702 | if not minSuccess then 703 | return false, minErrMsg or "" 704 | end 705 | 706 | local maxSuccess, maxErrMsg = maxCheck(value) 707 | if not maxSuccess then 708 | return false, maxErrMsg or "" 709 | end 710 | 711 | return true 712 | end 713 | end 714 | 715 | --[[** 716 | ensures value is a number where min < value < max 717 | 718 | @param min The minimum to use 719 | @param max The maximum to use 720 | 721 | @returns A function that will return true iff the condition is passed 722 | **--]] 723 | function t.numberConstrainedExclusive(min, max) 724 | assert(t.number(min)) 725 | assert(t.number(max)) 726 | local minCheck = t.numberMinExclusive(min) 727 | local maxCheck = t.numberMaxExclusive(max) 728 | 729 | return function(value) 730 | local minSuccess, minErrMsg = minCheck(value) 731 | if not minSuccess then 732 | return false, minErrMsg or "" 733 | end 734 | 735 | local maxSuccess, maxErrMsg = maxCheck(value) 736 | if not maxSuccess then 737 | return false, maxErrMsg or "" 738 | end 739 | 740 | return true 741 | end 742 | end 743 | 744 | --[[** 745 | ensures value matches string pattern 746 | 747 | @param string pattern to check against 748 | 749 | @returns A function that will return true iff the condition is passed 750 | **--]] 751 | function t.match(pattern) 752 | assert(t.string(pattern)) 753 | return function(value) 754 | local stringSuccess, stringErrMsg = t.string(value) 755 | if not stringSuccess then 756 | return false, stringErrMsg 757 | end 758 | 759 | if string.match(value, pattern) == nil then 760 | return false, string.format("%q failed to match pattern %q", value, pattern) 761 | end 762 | 763 | return true 764 | end 765 | end 766 | 767 | --[[** 768 | ensures value is either nil or passes check 769 | 770 | @param check The check to use 771 | 772 | @returns A function that will return true iff the condition is passed 773 | **--]] 774 | function t.optional(check) 775 | assert(t.callback(check)) 776 | return function(value) 777 | if value == nil then 778 | return true 779 | end 780 | 781 | local success, errMsg = check(value) 782 | if success then 783 | return true 784 | else 785 | return false, string.format("(optional) %s", errMsg or "") 786 | end 787 | end 788 | end 789 | 790 | --[[** 791 | matches given tuple against tuple type definition 792 | 793 | @param ... The type definition for the tuples 794 | 795 | @returns A function that will return true iff the condition is passed 796 | **--]] 797 | function t.tuple(...) 798 | local checks = { ... } 799 | return function(...) 800 | local args = { ... } 801 | for i, check in ipairs(checks) do 802 | local success, errMsg = check(args[i]) 803 | if success == false then 804 | return false, string.format("Bad tuple index #%s:\n\t%s", i, errMsg or "") 805 | end 806 | end 807 | 808 | return true 809 | end 810 | end 811 | 812 | --[[** 813 | ensures all keys in given table pass check 814 | 815 | @param check The function to use to check the keys 816 | 817 | @returns A function that will return true iff the condition is passed 818 | **--]] 819 | function t.keys(check) 820 | assert(t.callback(check)) 821 | return function(value) 822 | local tableSuccess, tableErrMsg = t.table(value) 823 | if tableSuccess == false then 824 | return false, tableErrMsg or "" 825 | end 826 | 827 | for key in pairs(value) do 828 | local success, errMsg = check(key) 829 | if success == false then 830 | return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "") 831 | end 832 | end 833 | 834 | return true 835 | end 836 | end 837 | 838 | --[[** 839 | ensures all values in given table pass check 840 | 841 | @param check The function to use to check the values 842 | 843 | @returns A function that will return true iff the condition is passed 844 | **--]] 845 | function t.values(check) 846 | assert(t.callback(check)) 847 | return function(value) 848 | local tableSuccess, tableErrMsg = t.table(value) 849 | if tableSuccess == false then 850 | return false, tableErrMsg or "" 851 | end 852 | 853 | for key, val in pairs(value) do 854 | local success, errMsg = check(val) 855 | if success == false then 856 | return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "") 857 | end 858 | end 859 | 860 | return true 861 | end 862 | end 863 | 864 | --[[** 865 | ensures value is a table and all keys pass keyCheck and all values pass valueCheck 866 | 867 | @param keyCheck The function to use to check the keys 868 | @param valueCheck The function to use to check the values 869 | 870 | @returns A function that will return true iff the condition is passed 871 | **--]] 872 | function t.map(keyCheck, valueCheck) 873 | assert(t.callback(keyCheck)) 874 | assert(t.callback(valueCheck)) 875 | local keyChecker = t.keys(keyCheck) 876 | local valueChecker = t.values(valueCheck) 877 | 878 | return function(value) 879 | local keySuccess, keyErr = keyChecker(value) 880 | if not keySuccess then 881 | return false, keyErr or "" 882 | end 883 | 884 | local valueSuccess, valueErr = valueChecker(value) 885 | if not valueSuccess then 886 | return false, valueErr or "" 887 | end 888 | 889 | return true 890 | end 891 | end 892 | 893 | --[[** 894 | ensures value is a table and all keys pass valueCheck and all values are true 895 | 896 | @param valueCheck The function to use to check the values 897 | 898 | @returns A function that will return true iff the condition is passed 899 | **--]] 900 | function t.set(valueCheck) 901 | return t.map(valueCheck, t.literal(true)) 902 | end 903 | 904 | do 905 | local arrayKeysCheck = t.keys(t.integer) 906 | --[[** 907 | ensures value is an array and all values of the array match check 908 | 909 | @param check The check to compare all values with 910 | 911 | @returns A function that will return true iff the condition is passed 912 | **--]] 913 | function t.array(check) 914 | assert(t.callback(check)) 915 | local valuesCheck = t.values(check) 916 | 917 | return function(value) 918 | local keySuccess, keyErrMsg = arrayKeysCheck(value) 919 | if keySuccess == false then 920 | return false, string.format("[array] %s", keyErrMsg or "") 921 | end 922 | 923 | -- # is unreliable for sparse arrays 924 | -- Count upwards using ipairs to avoid false positives from the behavior of # 925 | local arraySize = 0 926 | 927 | for _ in ipairs(value) do 928 | arraySize = arraySize + 1 929 | end 930 | 931 | for key in pairs(value) do 932 | if key < 1 or key > arraySize then 933 | return false, string.format("[array] key %s must be sequential", tostring(key)) 934 | end 935 | end 936 | 937 | local valueSuccess, valueErrMsg = valuesCheck(value) 938 | if not valueSuccess then 939 | return false, string.format("[array] %s", valueErrMsg or "") 940 | end 941 | 942 | return true 943 | end 944 | end 945 | 946 | --[[** 947 | ensures value is an array of a strict makeup and size 948 | 949 | @param check The check to compare all values with 950 | 951 | @returns A function that will return true iff the condition is passed 952 | **--]] 953 | function t.strictArray(...) 954 | local valueTypes = { ... } 955 | assert(t.array(t.callback)(valueTypes)) 956 | 957 | return function(value) 958 | local keySuccess, keyErrMsg = arrayKeysCheck(value) 959 | if keySuccess == false then 960 | return false, string.format("[strictArray] %s", keyErrMsg or "") 961 | end 962 | 963 | -- If there's more than the set array size, disallow 964 | if #valueTypes < #value then 965 | return false, string.format("[strictArray] Array size exceeds limit of %d", #valueTypes) 966 | end 967 | 968 | for idx, typeFn in pairs(valueTypes) do 969 | local typeSuccess, typeErrMsg = typeFn(value[idx]) 970 | if not typeSuccess then 971 | return false, string.format("[strictArray] Array index #%d - %s", idx, typeErrMsg) 972 | end 973 | end 974 | 975 | return true 976 | end 977 | end 978 | end 979 | 980 | do 981 | local callbackArray = t.array(t.callback) 982 | --[[** 983 | creates a union type 984 | 985 | @param ... The checks to union 986 | 987 | @returns A function that will return true iff the condition is passed 988 | **--]] 989 | function t.union(...) 990 | local checks = { ... } 991 | assert(callbackArray(checks)) 992 | 993 | return function(value) 994 | for _, check in ipairs(checks) do 995 | if check(value) then 996 | return true 997 | end 998 | end 999 | 1000 | return false, "bad type for union" 1001 | end 1002 | end 1003 | 1004 | --[[** 1005 | Alias for t.union 1006 | **--]] 1007 | t.some = t.union 1008 | 1009 | --[[** 1010 | creates an intersection type 1011 | 1012 | @param ... The checks to intersect 1013 | 1014 | @returns A function that will return true iff the condition is passed 1015 | **--]] 1016 | function t.intersection(...) 1017 | local checks = { ... } 1018 | assert(callbackArray(checks)) 1019 | 1020 | return function(value) 1021 | for _, check in ipairs(checks) do 1022 | local success, errMsg = check(value) 1023 | if not success then 1024 | return false, errMsg or "" 1025 | end 1026 | end 1027 | 1028 | return true 1029 | end 1030 | end 1031 | 1032 | --[[** 1033 | Alias for t.intersection 1034 | **--]] 1035 | t.every = t.intersection 1036 | end 1037 | 1038 | do 1039 | local checkInterface = t.map(t.any, t.callback) 1040 | --[[** 1041 | ensures value matches given interface definition 1042 | 1043 | @param checkTable The interface definition 1044 | 1045 | @returns A function that will return true iff the condition is passed 1046 | **--]] 1047 | function t.interface(checkTable) 1048 | assert(checkInterface(checkTable)) 1049 | return function(value) 1050 | local tableSuccess, tableErrMsg = t.table(value) 1051 | if tableSuccess == false then 1052 | return false, tableErrMsg or "" 1053 | end 1054 | 1055 | for key, check in pairs(checkTable) do 1056 | local success, errMsg = check(value[key]) 1057 | if success == false then 1058 | return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") 1059 | end 1060 | end 1061 | 1062 | return true 1063 | end 1064 | end 1065 | 1066 | --[[** 1067 | ensures value matches given interface definition strictly 1068 | 1069 | @param checkTable The interface definition 1070 | 1071 | @returns A function that will return true iff the condition is passed 1072 | **--]] 1073 | function t.strictInterface(checkTable) 1074 | assert(checkInterface(checkTable)) 1075 | return function(value) 1076 | local tableSuccess, tableErrMsg = t.table(value) 1077 | if tableSuccess == false then 1078 | return false, tableErrMsg or "" 1079 | end 1080 | 1081 | for key, check in pairs(checkTable) do 1082 | local success, errMsg = check(value[key]) 1083 | if success == false then 1084 | return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") 1085 | end 1086 | end 1087 | 1088 | for key in pairs(value) do 1089 | if not checkTable[key] then 1090 | return false, string.format("[interface] unexpected field %q", tostring(key)) 1091 | end 1092 | end 1093 | 1094 | return true 1095 | end 1096 | end 1097 | end 1098 | 1099 | --[[** 1100 | ensure value is an Instance and it's ClassName matches the given ClassName 1101 | 1102 | @param className The class name to check for 1103 | 1104 | @returns A function that will return true iff the condition is passed 1105 | **--]] 1106 | function t.instanceOf(className, childTable) 1107 | assert(t.string(className)) 1108 | 1109 | local childrenCheck 1110 | if childTable ~= nil then 1111 | childrenCheck = t.children(childTable) 1112 | end 1113 | 1114 | return function(value) 1115 | local instanceSuccess, instanceErrMsg = t.Instance(value) 1116 | if not instanceSuccess then 1117 | return false, instanceErrMsg or "" 1118 | end 1119 | 1120 | if value.ClassName ~= className then 1121 | return false, string.format("%s expected, got %s", className, value.ClassName) 1122 | end 1123 | 1124 | if childrenCheck then 1125 | local childrenSuccess, childrenErrMsg = childrenCheck(value) 1126 | if not childrenSuccess then 1127 | return false, childrenErrMsg 1128 | end 1129 | end 1130 | 1131 | return true 1132 | end 1133 | end 1134 | 1135 | t.instance = t.instanceOf 1136 | 1137 | --[[** 1138 | ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison 1139 | 1140 | @param className The class name to check for 1141 | 1142 | @returns A function that will return true iff the condition is passed 1143 | **--]] 1144 | function t.instanceIsA(className, childTable) 1145 | assert(t.string(className)) 1146 | 1147 | local childrenCheck 1148 | if childTable ~= nil then 1149 | childrenCheck = t.children(childTable) 1150 | end 1151 | 1152 | return function(value) 1153 | local instanceSuccess, instanceErrMsg = t.Instance(value) 1154 | if not instanceSuccess then 1155 | return false, instanceErrMsg or "" 1156 | end 1157 | 1158 | if not value:IsA(className) then 1159 | return false, string.format("%s expected, got %s", className, value.ClassName) 1160 | end 1161 | 1162 | if childrenCheck then 1163 | local childrenSuccess, childrenErrMsg = childrenCheck(value) 1164 | if not childrenSuccess then 1165 | return false, childrenErrMsg 1166 | end 1167 | end 1168 | 1169 | return true 1170 | end 1171 | end 1172 | 1173 | --[[** 1174 | ensures value is an enum of the correct type 1175 | 1176 | @param enum The enum to check 1177 | 1178 | @returns A function that will return true iff the condition is passed 1179 | **--]] 1180 | function t.enum(enum) 1181 | assert(t.Enum(enum)) 1182 | return function(value) 1183 | local enumItemSuccess, enumItemErrMsg = t.EnumItem(value) 1184 | if not enumItemSuccess then 1185 | return false, enumItemErrMsg 1186 | end 1187 | 1188 | if value.EnumType == enum then 1189 | return true 1190 | else 1191 | return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType)) 1192 | end 1193 | end 1194 | end 1195 | 1196 | do 1197 | local checkWrap = t.tuple(t.callback, t.callback) 1198 | 1199 | --[[** 1200 | wraps a callback in an assert with checkArgs 1201 | 1202 | @param callback The function to wrap 1203 | @param checkArgs The function to use to check arguments in the assert 1204 | 1205 | @returns A function that first asserts using checkArgs and then calls callback 1206 | **--]] 1207 | function t.wrap(callback, checkArgs) 1208 | assert(checkWrap(callback, checkArgs)) 1209 | return function(...) 1210 | assert(checkArgs(...)) 1211 | return callback(...) 1212 | end 1213 | end 1214 | end 1215 | 1216 | --[[** 1217 | asserts a given check 1218 | 1219 | @param check The function to wrap with an assert 1220 | 1221 | @returns A function that simply wraps the given check in an assert 1222 | **--]] 1223 | function t.strict(check) 1224 | return function(...) 1225 | assert(check(...)) 1226 | end 1227 | end 1228 | 1229 | do 1230 | local checkChildren = t.map(t.string, t.callback) 1231 | 1232 | --[[** 1233 | Takes a table where keys are child names and values are functions to check the children against. 1234 | Pass an instance tree into the function. 1235 | If at least one child passes each check, the overall check passes. 1236 | 1237 | Warning! If you pass in a tree with more than one child of the same name, this function will always return false 1238 | 1239 | @param checkTable The table to check against 1240 | 1241 | @returns A function that checks an instance tree 1242 | **--]] 1243 | function t.children(checkTable) 1244 | assert(checkChildren(checkTable)) 1245 | 1246 | return function(value) 1247 | local instanceSuccess, instanceErrMsg = t.Instance(value) 1248 | if not instanceSuccess then 1249 | return false, instanceErrMsg or "" 1250 | end 1251 | 1252 | local childrenByName = {} 1253 | for _, child in ipairs(value:GetChildren()) do 1254 | local name = child.Name 1255 | if checkTable[name] then 1256 | if childrenByName[name] then 1257 | return false, string.format("Cannot process multiple children with the same name %q", name) 1258 | end 1259 | 1260 | childrenByName[name] = child 1261 | end 1262 | end 1263 | 1264 | for name, check in pairs(checkTable) do 1265 | local success, errMsg = check(childrenByName[name]) 1266 | if not success then 1267 | return false, string.format("[%s.%s] %s", value:GetFullName(), name, errMsg or "") 1268 | end 1269 | end 1270 | 1271 | return true 1272 | end 1273 | end 1274 | end 1275 | 1276 | return t 1277 | -------------------------------------------------------------------------------- /lib/init.spec.lua: -------------------------------------------------------------------------------- 1 | return function() 2 | local t = require(script.Parent) 3 | 4 | it("should support basic types", function() 5 | assert(t.any("")) 6 | assert(t.boolean(true)) 7 | assert(t.none(nil)) 8 | assert(t.number(1)) 9 | assert(t.string("foo")) 10 | assert(t.table({})) 11 | 12 | assert(not (t.any(nil))) 13 | assert(not (t.boolean("true"))) 14 | assert(not (t.none(1))) 15 | assert(not (t.number(true))) 16 | assert(not (t.string(true))) 17 | assert(not (t.table(82))) 18 | end) 19 | 20 | it("should support special number types", function() 21 | local maxTen = t.numberMax(10) 22 | local minTwo = t.numberMin(2) 23 | local maxTenEx = t.numberMaxExclusive(10) 24 | local minTwoEx = t.numberMinExclusive(2) 25 | local constrainedEightToEleven = t.numberConstrained(8, 11) 26 | local constrainedEightToElevenEx = t.numberConstrainedExclusive(8, 11) 27 | 28 | assert(maxTen(5)) 29 | assert(maxTen(10)) 30 | assert(not (maxTen(11))) 31 | assert(not (maxTen())) 32 | 33 | assert(minTwo(5)) 34 | assert(minTwo(2)) 35 | assert(not (minTwo(1))) 36 | assert(not (minTwo())) 37 | 38 | assert(maxTenEx(5)) 39 | assert(maxTenEx(9)) 40 | assert(not (maxTenEx(10))) 41 | assert(not (maxTenEx())) 42 | 43 | assert(minTwoEx(5)) 44 | assert(minTwoEx(3)) 45 | assert(not (minTwoEx(2))) 46 | assert(not (minTwoEx())) 47 | 48 | assert(not (constrainedEightToEleven(7))) 49 | assert(constrainedEightToEleven(8)) 50 | assert(constrainedEightToEleven(9)) 51 | assert(constrainedEightToEleven(11)) 52 | assert(not (constrainedEightToEleven(12))) 53 | assert(not (constrainedEightToEleven())) 54 | 55 | assert(not (constrainedEightToElevenEx(7))) 56 | assert(not (constrainedEightToElevenEx(8))) 57 | assert(constrainedEightToElevenEx(9)) 58 | assert(not (constrainedEightToElevenEx(11))) 59 | assert(not (constrainedEightToElevenEx(12))) 60 | assert(not (constrainedEightToElevenEx())) 61 | end) 62 | 63 | it("should support optional types", function() 64 | local check = t.optional(t.string) 65 | assert(check("")) 66 | assert(check()) 67 | assert(not (check(1))) 68 | end) 69 | 70 | it("should support tuple types", function() 71 | local myTupleCheck = t.tuple(t.number, t.string, t.optional(t.number)) 72 | assert(myTupleCheck(1, "2", 3)) 73 | assert(myTupleCheck(1, "2")) 74 | assert(not (myTupleCheck(1, "2", "3"))) 75 | end) 76 | 77 | it("should support union types", function() 78 | local numberOrString = t.union(t.number, t.string) 79 | assert(numberOrString(1)) 80 | assert(numberOrString("1")) 81 | assert(not (numberOrString(nil))) 82 | end) 83 | 84 | it("should support literal types", function() 85 | local checkSingle = t.literal("foo") 86 | local checkUnion = t.union(t.literal("foo"), t.literal("bar"), t.literal("oof")) 87 | 88 | assert(checkSingle("foo")) 89 | assert(checkUnion("foo")) 90 | assert(checkUnion("bar")) 91 | assert(checkUnion("oof")) 92 | 93 | assert(not (checkSingle("FOO"))) 94 | assert(not (checkUnion("FOO"))) 95 | assert(not (checkUnion("BAR"))) 96 | assert(not (checkUnion("OOF"))) 97 | end) 98 | 99 | it("should support multiple literal types", function() 100 | local checkSingle = t.literal("foo") 101 | local checkUnion = t.literal("foo", "bar", "oof") 102 | 103 | assert(checkSingle("foo")) 104 | assert(checkUnion("foo")) 105 | assert(checkUnion("bar")) 106 | assert(checkUnion("oof")) 107 | 108 | assert(not (checkSingle("FOO"))) 109 | assert(not (checkUnion("FOO"))) 110 | assert(not (checkUnion("BAR"))) 111 | assert(not (checkUnion("OOF"))) 112 | end) 113 | 114 | it("should support intersection types", function() 115 | local integerMax5000 = t.intersection(t.integer, t.numberMax(5000)) 116 | assert(integerMax5000(1)) 117 | assert(not (integerMax5000(5001))) 118 | assert(not (integerMax5000(1.1))) 119 | assert(not (integerMax5000("1"))) 120 | end) 121 | 122 | describe("array", function() 123 | it("should support array types", function() 124 | local stringArray = t.array(t.string) 125 | local anyArray = t.array(t.any) 126 | local stringValues = t.values(t.string) 127 | assert(not (anyArray("foo"))) 128 | assert(anyArray({1, "2", 3})) 129 | assert(not (stringArray({1, "2", 3}))) 130 | assert(not (stringArray())) 131 | assert(not (stringValues())) 132 | assert(anyArray({"1", "2", "3"}, t.string)) 133 | assert(not (anyArray({ 134 | foo = "bar" 135 | }))) 136 | assert(not (anyArray({ 137 | [1] = "non", 138 | [5] = "sequential" 139 | }))) 140 | end) 141 | 142 | it("should not be fooled by sparse arrays", function() 143 | local anyArray = t.array(t.any) 144 | 145 | assert(not (anyArray({ 146 | [1] = 1, 147 | [2] = 2, 148 | [4] = 4, 149 | }))) 150 | end) 151 | end) 152 | 153 | it("should support map types", function() 154 | local stringNumberMap = t.map(t.string, t.number) 155 | assert(stringNumberMap({})) 156 | assert(stringNumberMap({a = 1})) 157 | assert(not (stringNumberMap({[1] = "a"}))) 158 | assert(not (stringNumberMap({a = "a"}))) 159 | assert(not (stringNumberMap())) 160 | end) 161 | 162 | it("should support set types", function() 163 | local stringSet = t.set(t.string) 164 | assert(stringSet({})) 165 | assert(stringSet({a = true})) 166 | assert(not (stringSet({[1] = "a"}))) 167 | assert(not (stringSet({a = "a"}))) 168 | assert(not (stringSet({a = false}))) 169 | assert(not (stringSet())) 170 | end) 171 | 172 | it("should support interface types", function() 173 | local IVector3 = t.interface({ 174 | x = t.number, 175 | y = t.number, 176 | z = t.number, 177 | }) 178 | 179 | assert(IVector3({ 180 | w = 0, 181 | x = 1, 182 | y = 2, 183 | z = 3, 184 | })) 185 | 186 | assert(not (IVector3({ 187 | w = 0, 188 | x = 1, 189 | y = 2, 190 | }))) 191 | end) 192 | 193 | it("should support strict interface types", function() 194 | local IVector3 = t.strictInterface({ 195 | x = t.number, 196 | y = t.number, 197 | z = t.number, 198 | }) 199 | 200 | assert(not (IVector3(0))) 201 | 202 | assert(not (IVector3({ 203 | w = 0, 204 | x = 1, 205 | y = 2, 206 | z = 3, 207 | }))) 208 | 209 | assert(not (IVector3({ 210 | w = 0, 211 | x = 1, 212 | y = 2, 213 | }))) 214 | 215 | assert(IVector3({ 216 | x = 1, 217 | y = 2, 218 | z = 3, 219 | })) 220 | end) 221 | 222 | it("should support deep interface types", function() 223 | local IPlayer = t.interface({ 224 | name = t.string, 225 | inventory = t.interface({ 226 | size = t.number 227 | }) 228 | }) 229 | 230 | assert(IPlayer({ 231 | name = "TestPlayer", 232 | inventory = { 233 | size = 1 234 | } 235 | })) 236 | 237 | assert(not (IPlayer({ 238 | inventory = { 239 | size = 1 240 | } 241 | }))) 242 | 243 | assert(not (IPlayer({ 244 | name = "TestPlayer", 245 | inventory = { 246 | } 247 | }))) 248 | 249 | assert(not (IPlayer({ 250 | name = "TestPlayer", 251 | }))) 252 | end) 253 | 254 | it("should support deep optional interface types", function() 255 | local IPlayer = t.interface({ 256 | name = t.string, 257 | inventory = t.optional(t.interface({ 258 | size = t.number 259 | })) 260 | }) 261 | 262 | assert(IPlayer({ 263 | name = "TestPlayer" 264 | })) 265 | 266 | assert(not (IPlayer({ 267 | name = "TestPlayer", 268 | inventory = { 269 | } 270 | }))) 271 | 272 | assert(IPlayer({ 273 | name = "TestPlayer", 274 | inventory = { 275 | size = 1 276 | } 277 | })) 278 | end) 279 | 280 | it("should support Roblox Instance types", function() 281 | local stringValueCheck = t.instanceOf("StringValue") 282 | local stringValue = Instance.new("StringValue") 283 | local boolValue = Instance.new("BoolValue") 284 | 285 | assert(stringValueCheck(stringValue)) 286 | assert(not (stringValueCheck(boolValue))) 287 | assert(not (stringValueCheck())) 288 | end) 289 | 290 | it("should support Roblox Instance types inheritance", function() 291 | local guiObjectCheck = t.instanceIsA("GuiObject") 292 | local frame = Instance.new("Frame") 293 | local textLabel = Instance.new("TextLabel") 294 | local stringValue = Instance.new("StringValue") 295 | 296 | assert(guiObjectCheck(frame)) 297 | assert(guiObjectCheck(textLabel)) 298 | assert(not (guiObjectCheck(stringValue))) 299 | assert(not (guiObjectCheck())) 300 | end) 301 | 302 | it("should support Roblox Enum types", function() 303 | local sortOrderEnumCheck = t.enum(Enum.SortOrder) 304 | assert(t.Enum(Enum.SortOrder)) 305 | assert(not (t.Enum("Enum.SortOrder"))) 306 | 307 | assert(t.EnumItem(Enum.SortOrder.Name)) 308 | assert(not (t.EnumItem("Enum.SortOrder.Name"))) 309 | 310 | assert(sortOrderEnumCheck(Enum.SortOrder.Name)) 311 | assert(sortOrderEnumCheck(Enum.SortOrder.Custom)) 312 | assert(not (sortOrderEnumCheck(Enum.EasingStyle.Linear))) 313 | assert(not (sortOrderEnumCheck())) 314 | end) 315 | 316 | it("should support Roblox RBXScriptSignal", function() 317 | assert(t.RBXScriptSignal(game.ChildAdded)) 318 | assert(not (t.RBXScriptSignal(nil))) 319 | assert(not (t.RBXScriptSignal(Vector3.new()))) 320 | end) 321 | 322 | -- TODO: Add this back when Lemur supports it 323 | -- it("should support Roblox RBXScriptConnection", function() 324 | -- local conn = game.ChildAdded:Connect(function() end) 325 | -- assert(t.RBXScriptConnection(conn)) 326 | -- assert(not (t.RBXScriptConnection(nil))) 327 | -- assert(not (t.RBXScriptConnection(Vector3.new()))) 328 | -- end) 329 | 330 | it("should support wrapping function types", function() 331 | local checkFoo = t.tuple(t.string, t.number, t.optional(t.string)) 332 | local foo = t.wrap(function(a, b, c) 333 | local result = string.format("%s %d", a, b) 334 | if c then 335 | result = result .. " " .. c 336 | end 337 | return result 338 | end, checkFoo) 339 | 340 | assert(not (pcall(foo))) 341 | assert(not (pcall(foo, "a"))) 342 | assert(not (pcall(foo, 2))) 343 | assert(pcall(foo, "a", 1)) 344 | assert(pcall(foo, "a", 1, "b")) 345 | end) 346 | 347 | it("should support strict types", function() 348 | local myType = t.strict(t.tuple(t.string, t.number)) 349 | assert(not (pcall(function() 350 | myType("a", "b") 351 | end))) 352 | assert(pcall(function() 353 | myType("a", 1) 354 | end)) 355 | end) 356 | 357 | it("should support common OOP types", function() 358 | local MyClass = {} 359 | MyClass.__index = MyClass 360 | 361 | function MyClass.new() 362 | local self = setmetatable({}, MyClass) 363 | return self 364 | end 365 | 366 | local function instanceOfClass(class) 367 | return function(value) 368 | local tableSuccess, tableErrMsg = t.table(value) 369 | if not tableSuccess then 370 | return false, tableErrMsg or "" 371 | end 372 | 373 | local mt = getmetatable(value) 374 | if not mt or mt.__index ~= class then 375 | return false, "bad member of class" 376 | end 377 | 378 | return true 379 | end 380 | end 381 | 382 | local instanceOfMyClass = instanceOfClass(MyClass) 383 | 384 | local myObject = MyClass.new() 385 | assert(instanceOfMyClass(myObject)) 386 | assert(not (instanceOfMyClass({}))) 387 | assert(not (instanceOfMyClass())) 388 | end) 389 | 390 | it("should not treat NaN as numbers", function() 391 | assert(t.number(1)) 392 | assert(not (t.number(0/0))) 393 | assert(not (t.number("1"))) 394 | end) 395 | 396 | it("should not treat numbers as NaN", function() 397 | assert(not (t.nan(1))) 398 | assert(t.nan(0/0)) 399 | assert(not (t.nan("1"))) 400 | end) 401 | 402 | it("should allow union of number and NaN", function() 403 | local numberOrNaN = t.union(t.number, t.nan) 404 | assert(numberOrNaN(1)) 405 | assert(numberOrNaN(0/0)) 406 | assert(not (numberOrNaN("1"))) 407 | end) 408 | 409 | it("should support non-string keys for interfaces", function() 410 | local key = {} 411 | local myInterface = t.interface({ [key] = t.number }) 412 | assert(myInterface({ [key] = 1 })) 413 | assert(not (myInterface({ [key] = "1" }))) 414 | end) 415 | 416 | it("should support failing on non-string keys for strict interfaces", function() 417 | local myInterface = t.strictInterface({ a = t.number }) 418 | assert(not (myInterface({ a = 1, [{}] = 2 }))) 419 | end) 420 | 421 | it("should support children", function() 422 | local myInterface = t.interface({ 423 | buttonInFrame = t.intersection(t.instanceOf("Frame"), t.children({ 424 | MyButton = t.instanceOf("ImageButton") 425 | })) 426 | }) 427 | 428 | assert(not (t.children({})(5))) 429 | assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) 430 | 431 | do 432 | local frame = Instance.new("Frame") 433 | local button = Instance.new("ImageButton", frame) 434 | button.Name = "MyButton" 435 | assert(myInterface({ buttonInFrame = frame })) 436 | end 437 | 438 | do 439 | local frame = Instance.new("Frame") 440 | local button = Instance.new("ImageButton", frame) 441 | button.Name = "NotMyButton" 442 | assert(not (myInterface({ buttonInFrame = frame }))) 443 | end 444 | 445 | do 446 | local frame = Instance.new("Frame") 447 | local button = Instance.new("TextButton", frame) 448 | button.Name = "MyButton" 449 | assert(not (myInterface({ buttonInFrame = frame }))) 450 | end 451 | 452 | do 453 | local frame = Instance.new("Frame") 454 | local button1 = Instance.new("ImageButton", frame) 455 | button1.Name = "MyButton" 456 | local button2 = Instance.new("ImageButton", frame) 457 | button2.Name = "MyButton" 458 | assert(not (myInterface({ buttonInFrame = frame }))) 459 | end 460 | end) 461 | 462 | it("should support t.instanceOf shorthand", function() 463 | local myInterface = t.interface({ 464 | buttonInFrame = t.instanceOf("Frame", { 465 | MyButton = t.instanceOf("ImageButton") 466 | }) 467 | }) 468 | 469 | assert(not (t.children({})(5))) 470 | assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) 471 | 472 | do 473 | local frame = Instance.new("Frame") 474 | local button = Instance.new("ImageButton", frame) 475 | button.Name = "MyButton" 476 | assert(myInterface({ buttonInFrame = frame })) 477 | end 478 | 479 | do 480 | local frame = Instance.new("Frame") 481 | local button = Instance.new("ImageButton", frame) 482 | button.Name = "NotMyButton" 483 | assert(not (myInterface({ buttonInFrame = frame }))) 484 | end 485 | 486 | do 487 | local frame = Instance.new("Frame") 488 | local button = Instance.new("TextButton", frame) 489 | button.Name = "MyButton" 490 | assert(not (myInterface({ buttonInFrame = frame }))) 491 | end 492 | 493 | do 494 | local frame = Instance.new("Frame") 495 | local button1 = Instance.new("ImageButton", frame) 496 | button1.Name = "MyButton" 497 | local button2 = Instance.new("ImageButton", frame) 498 | button2.Name = "MyButton" 499 | assert(not (myInterface({ buttonInFrame = frame }))) 500 | end 501 | end) 502 | 503 | it("should support t.instanceIsA shorthand", function() 504 | local myInterface = t.interface({ 505 | buttonInFrame = t.instanceIsA("Frame", { 506 | MyButton = t.instanceIsA("ImageButton") 507 | }) 508 | }) 509 | 510 | assert(not (t.children({})(5))) 511 | assert(not (myInterface({ buttonInFrame = Instance.new("Frame") }))) 512 | 513 | do 514 | local frame = Instance.new("Frame") 515 | local button = Instance.new("ImageButton", frame) 516 | button.Name = "MyButton" 517 | assert(myInterface({ buttonInFrame = frame })) 518 | end 519 | 520 | do 521 | local frame = Instance.new("Frame") 522 | local button = Instance.new("ImageButton", frame) 523 | button.Name = "NotMyButton" 524 | assert(not (myInterface({ buttonInFrame = frame }))) 525 | end 526 | 527 | do 528 | local frame = Instance.new("Frame") 529 | local button = Instance.new("TextButton", frame) 530 | button.Name = "MyButton" 531 | assert(not (myInterface({ buttonInFrame = frame }))) 532 | end 533 | 534 | do 535 | local frame = Instance.new("Frame") 536 | local button1 = Instance.new("ImageButton", frame) 537 | button1.Name = "MyButton" 538 | local button2 = Instance.new("ImageButton", frame) 539 | button2.Name = "MyButton" 540 | assert(not (myInterface({ buttonInFrame = frame }))) 541 | end 542 | end) 543 | 544 | it("should support t.match", function() 545 | local check = t.match("%d+") 546 | assert(check("123")) 547 | assert(not (check("abc"))) 548 | assert(not (check())) 549 | end) 550 | 551 | it("should support t.keyOf", function() 552 | local myNewEnum = { 553 | OptionA = {}, 554 | OptionB = {}, 555 | } 556 | local check = t.keyOf(myNewEnum) 557 | assert(check("OptionA")) 558 | assert(not (check("OptionC"))) 559 | end) 560 | 561 | it("should support t.valueOf", function() 562 | local myNewEnum = { 563 | OptionA = {}, 564 | OptionB = {}, 565 | } 566 | local check = t.valueOf(myNewEnum) 567 | assert(check(myNewEnum.OptionA)) 568 | assert(not (check(1010))) 569 | end) 570 | 571 | it("should support t.strictArray", function() 572 | local fixedArrayCheck = t.strictArray(t.number, t.number) 573 | 574 | assert(fixedArrayCheck({1, 2})) 575 | assert(not fixedArrayCheck({1, 2, 3})) 576 | assert(not fixedArrayCheck({10})) 577 | assert(not fixedArrayCheck({"Hello", 10})) 578 | assert(not fixedArrayCheck({ Foo = "Bar" })) 579 | 580 | local fixedArrayCheck2 = t.strictArray(t.number, t.number, t.optional(t.string)) 581 | 582 | assert(fixedArrayCheck2({10, 20})) 583 | assert(fixedArrayCheck2({10, 20, "Hello"})) 584 | assert(not fixedArrayCheck2({10, 20, 30})) 585 | end) 586 | end -------------------------------------------------------------------------------- /lib/t.d.ts: -------------------------------------------------------------------------------- 1 | // utility types 2 | type Literal = string | number | boolean | undefined | null | void | {}; 3 | type ArrayType = T extends Array ? U : never; 4 | 5 | interface t { 6 | // lua types 7 | /** checks to see if `type(value) == typeName` */ 8 | type: (typeName: T) => t.check; 9 | /** checks to see if `typeof(value) == typeName` */ 10 | typeof: (typeName: T) => t.check; 11 | /** checks to see if `value` is not undefined */ 12 | any: t.check; 13 | /** checks to see if `value` is a boolean */ 14 | boolean: t.check; 15 | /** checks to see if `value` is a thread */ 16 | thread: t.check; 17 | /** checks to see if `value` is a function */ 18 | callback: t.check<(...args: Array) => unknown>; 19 | /** alias of t.callback */ 20 | function: t.check; 21 | /** checks to see if `value` is undefined */ 22 | none: t.check; 23 | /** alias of t.none */ 24 | nil: t.check; 25 | /** checks to see if `value` is a number, will _not_ match NaN */ 26 | number: t.check; 27 | /** checks to see if `value` is NaN */ 28 | nan: t.check; 29 | /** checks to see if `value` is a string */ 30 | string: t.check; 31 | /** checks to see if `value` is an object */ 32 | table: t.check; 33 | /** checks to see if `value` is a userdata */ 34 | userdata: t.check; 35 | 36 | // roblox types 37 | /** checks to see if `value` is an Axes */ 38 | Axes: t.check; 39 | /** checks to see if `value` is a BrickColor */ 40 | BrickColor: t.check; 41 | /** checks to see if `value` is a CatalogSearchParams */ 42 | CatalogSearchParams: t.check; 43 | /** checks to see if `value` is a CFrame */ 44 | CFrame: t.check; 45 | /** checks to see if `value` is a Color3 */ 46 | Color3: t.check; 47 | /** checks to see if `value` is a ColorSequence */ 48 | ColorSequence: t.check; 49 | /** checks to see if `value` is a ColorSequenceKeypoint */ 50 | ColorSequenceKeypoint: t.check; 51 | /** checks to see if `value` is a DateTime */ 52 | DateTime: t.check; 53 | /** checks to see if `value` is a DockWidgetPluginGuiInfo */ 54 | DockWidgetPluginGuiInfo: t.check; 55 | /** checks to see if `value` is an Enum */ 56 | Enum: t.check; 57 | /** checks to see if `value` is an EnumItem */ 58 | EnumItem: t.check; 59 | /** checks to see if `value` is an Enums */ 60 | Enums: t.check; 61 | /** checks to see if `value` is a Faces */ 62 | Faces: t.check; 63 | /** checks to see if `value` is a FloatCurveKey */ 64 | FloatCurveKey: t.check; 65 | /** checks to see if `value` is a Font */ 66 | Font: t.check; 67 | /** checks to see if `value` is an Instance */ 68 | Instance: t.check; 69 | /** checks to see if `value` is a NumberRange */ 70 | NumberRange: t.check; 71 | /** checks to see if `value` is a NumberSequence */ 72 | NumberSequence: t.check; 73 | /** checks to see if `value` is a NumberSequenceKeypoint */ 74 | NumberSequenceKeypoint: t.check; 75 | /** checks to see if `value` is a OverlapParams */ 76 | OverlapParams: t.check; 77 | /** checks to see if `value` is a PathWaypoint */ 78 | PathWaypoint: t.check; 79 | /** checks to see if `value` is a PhysicalProperties */ 80 | PhysicalProperties: t.check; 81 | /** checks to see if `value` is a Random */ 82 | Random: t.check; 83 | /** checks to see if `value` is a Ray */ 84 | Ray: t.check; 85 | /** checks to see if `value` is a RaycastParams */ 86 | RaycastParams: t.check; 87 | /** checks to see if `value` is a RaycastResult */ 88 | RaycastResult: t.check; 89 | /** checks to see if `value` is a RBXScriptConnection */ 90 | RBXScriptConnection: t.check; 91 | /** checks to see if `value` is a RBXScriptSignal */ 92 | RBXScriptSignal: t.check; 93 | /** checks to see if `value` is a Rect */ 94 | Rect: t.check; 95 | /** checks to see if `value` is a Region3 */ 96 | Region3: t.check; 97 | /** checks to see if `value` is a Region3int16 */ 98 | Region3int16: t.check; 99 | /** checks to see if `value` is a TweenInfo */ 100 | TweenInfo: t.check; 101 | /** checks to see if `value` is a UDim */ 102 | UDim: t.check; 103 | /** checks to see if `value` is a UDim2 */ 104 | UDim2: t.check; 105 | /** checks to see if `value` is a Vector2 */ 106 | Vector2: t.check; 107 | /** checks to see if `value` is a Vector2int16 */ 108 | Vector2int16: t.check; 109 | /** checks to see if `value` is a Vector3 */ 110 | Vector3: t.check; 111 | /** checks to see if `value` is a Vector3int16 */ 112 | Vector3int16: t.check; 113 | 114 | /** checks if `value` is an EnumItem which belongs to `Enum`. */ 115 | enum: (Enum: T) => t.check>; 116 | 117 | // type functions 118 | /** checks to see if `value == literalValue` */ 119 | literal>(this: void, ...args: T): t.check>; 120 | /** returns a t.union of each key in the table as a t.literal */ 121 | keyOf: (valueTable: T) => t.check; 122 | /** returns a t.union of each value in the table as a t.literal */ 123 | valueOf: (valueTable: T) => T extends { [P in keyof T]: infer U } ? t.check : never; 124 | /** checks to see if `value` is an integer */ 125 | integer: t.check; 126 | /** checks to see if `value` is a number and is more than or equal to `min` */ 127 | numberMin: (min: number) => t.check; 128 | /** checks to see if `value` is a number and is less than or equal to `max` */ 129 | numberMax: (max: number) => t.check; 130 | /** checks to see if `value` is a number and is more than `min` */ 131 | numberMinExclusive: (min: number) => t.check; 132 | /** checks to see if `value` is a number and is less than `max` */ 133 | numberMaxExclusive: (max: number) => t.check; 134 | /** checks to see if `value` is a number and is more than 0 */ 135 | numberPositive: t.check; 136 | /** checks to see if `value` is a number and is less than 0 */ 137 | numberNegative: t.check; 138 | /** checks to see if `value` is a number and `min <= value <= max` */ 139 | numberConstrained: (min: number, max: number) => t.check; 140 | /** checks to see if `value` is a number and `min < value < max` */ 141 | numberConstrainedExclusive: (min: number, max: number) => t.check; 142 | /** checks `t.string` and determines if value matches the pattern via `string.match(value, pattern)` */ 143 | match: (pattern: string) => t.check; 144 | /** checks to see if `value` is either nil or passes `check` */ 145 | optional: (check: t.check) => t.check; 146 | /** checks to see if `value` is a table and if its keys match against `check */ 147 | keys: (check: t.check) => t.check>; 148 | /** checks to see if `value` is a table and if its values match against `check` */ 149 | values: (check: t.check) => t.check>; 150 | /** checks to see if `value` is a table and all of its keys match against `keyCheck` and all of its values match against `valueCheck` */ 151 | map: (keyCheck: t.check, valueCheck: t.check) => t.check>; 152 | /** checks to see if `value` is a table and all of its keys match against `valueCheck` and all of its values are `true` */ 153 | set: (valueCheck: t.check) => t.check>; 154 | /** checks to see if `value` is an array and all of its keys are sequential integers and all of its values match `check` */ 155 | array: (check: t.check) => t.check>; 156 | /** ensures value is an array of a strict makeup and size */ 157 | strictArray: >>(...args: T) => t.check<{ [K in keyof T]: t.static }>; 158 | /** checks to see if `value` matches any given check */ 159 | union: >>(...args: T) => t.check>>; 160 | /** checks to see if `value` matches all given checks */ 161 | intersection: >>( 162 | ...args: T 163 | ) => T[Exclude | "length">] extends infer U 164 | ? (U extends any ? (k: U) => void : never) extends (k: t.check) => void 165 | ? t.check 166 | : never 167 | : never; 168 | /** checks to see if `value` matches a given interface definition */ 169 | interface: }>( 170 | checkTable: T, 171 | ) => t.check<{ [P in keyof T]: t.static }>; 172 | /** checks to see if `value` matches a given interface definition with no extra members */ 173 | strictInterface: }>( 174 | checkTable: T, 175 | ) => t.check<{ [P in keyof T]: t.static }>; 176 | /** ensure value is an Instance and it's ClassName matches the given ClassName */ 177 | instanceOf(this: void, className: S): t.check; 178 | instanceOf }>( 179 | this: void, 180 | className: S, 181 | checkTable: T, 182 | ): t.check }>; 183 | /** ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison */ 184 | instanceIsA(this: void, className: S): t.check; 185 | instanceIsA }>( 186 | this: void, 187 | className: S, 188 | checkTable: T, 189 | ): t.check }>; 190 | /** 191 | * Takes a table where keys are child names and values are functions to check the children against. 192 | * Pass an instance tree into the function. 193 | * If at least one child passes each check, the overall check passes. 194 | * 195 | * Warning! If you pass in a tree with more than one child of the same name, this function will always return false 196 | */ 197 | children: }>( 198 | checkTable: T, 199 | ) => t.check }>; 200 | } 201 | 202 | declare namespace t { 203 | /** creates a static type from a t-defined type */ 204 | export type static = T extends t.check ? U : never; 205 | 206 | /** checks to see if `value` is a T */ 207 | export type check = (value: unknown) => value is T; 208 | } 209 | 210 | declare const t: t; 211 | export { t }; 212 | -------------------------------------------------------------------------------- /lib/ts.lua: -------------------------------------------------------------------------------- 1 | -- t: a runtime typechecker for Roblox 2 | 3 | local t = {} 4 | 5 | function t.type(typeName) 6 | return function(value) 7 | local valueType = type(value) 8 | if valueType == typeName then 9 | return true 10 | else 11 | return false 12 | end 13 | end 14 | end 15 | 16 | function t.typeof(typeName) 17 | return function(value) 18 | local valueType = typeof(value) 19 | if valueType == typeName then 20 | return true 21 | else 22 | return false 23 | end 24 | end 25 | end 26 | 27 | --[[** 28 | matches any type except nil 29 | 30 | @param value The value to check against 31 | 32 | @returns True iff the condition is satisfied, false otherwise 33 | **--]] 34 | function t.any(value) 35 | if value ~= nil then 36 | return true 37 | else 38 | return false 39 | end 40 | end 41 | 42 | --Lua primitives 43 | 44 | --[[** 45 | ensures Lua primitive boolean type 46 | 47 | @param value The value to check against 48 | 49 | @returns True iff the condition is satisfied, false otherwise 50 | **--]] 51 | t.boolean = t.typeof("boolean") 52 | 53 | --[[** 54 | ensures Lua primitive thread type 55 | 56 | @param value The value to check against 57 | 58 | @returns True iff the condition is satisfied, false otherwise 59 | **--]] 60 | t.thread = t.typeof("thread") 61 | 62 | --[[** 63 | ensures Lua primitive callback type 64 | 65 | @param value The value to check against 66 | 67 | @returns True iff the condition is satisfied, false otherwise 68 | **--]] 69 | t.callback = t.typeof("function") 70 | t["function"] = t.callback 71 | 72 | --[[** 73 | ensures Lua primitive none type 74 | 75 | @param value The value to check against 76 | 77 | @returns True iff the condition is satisfied, false otherwise 78 | **--]] 79 | t.none = t.typeof("nil") 80 | t["nil"] = t.none 81 | 82 | --[[** 83 | ensures Lua primitive string type 84 | 85 | @param value The value to check against 86 | 87 | @returns True iff the condition is satisfied, false otherwise 88 | **--]] 89 | t.string = t.typeof("string") 90 | 91 | --[[** 92 | ensures Lua primitive table type 93 | 94 | @param value The value to check against 95 | 96 | @returns True iff the condition is satisfied, false otherwise 97 | **--]] 98 | t.table = t.typeof("table") 99 | 100 | --[[** 101 | ensures Lua primitive userdata type 102 | 103 | @param value The value to check against 104 | 105 | @returns True iff the condition is satisfied, false otherwise 106 | **--]] 107 | t.userdata = t.type("userdata") 108 | 109 | --[[** 110 | ensures value is a number and non-NaN 111 | 112 | @param value The value to check against 113 | 114 | @returns True iff the condition is satisfied, false otherwise 115 | **--]] 116 | function t.number(value) 117 | local valueType = typeof(value) 118 | if valueType == "number" then 119 | if value == value then 120 | return true 121 | else 122 | return false 123 | end 124 | else 125 | return false 126 | end 127 | end 128 | 129 | --[[** 130 | ensures value is NaN 131 | 132 | @param value The value to check against 133 | 134 | @returns True iff the condition is satisfied, false otherwise 135 | **--]] 136 | function t.nan(value) 137 | local valueType = typeof(value) 138 | if valueType == "number" then 139 | if value ~= value then 140 | return true 141 | else 142 | return false 143 | end 144 | else 145 | return false 146 | end 147 | end 148 | 149 | -- roblox types 150 | 151 | --[[** 152 | ensures Roblox Axes type 153 | 154 | @param value The value to check against 155 | 156 | @returns True iff the condition is satisfied, false otherwise 157 | **--]] 158 | t.Axes = t.typeof("Axes") 159 | 160 | --[[** 161 | ensures Roblox BrickColor type 162 | 163 | @param value The value to check against 164 | 165 | @returns True iff the condition is satisfied, false otherwise 166 | **--]] 167 | t.BrickColor = t.typeof("BrickColor") 168 | 169 | --[[** 170 | ensures Roblox CatalogSearchParams type 171 | 172 | @param value The value to check against 173 | 174 | @returns True iff the condition is satisfied, false otherwise 175 | **--]] 176 | t.CatalogSearchParams = t.typeof("CatalogSearchParams") 177 | 178 | --[[** 179 | ensures Roblox CFrame type 180 | 181 | @param value The value to check against 182 | 183 | @returns True iff the condition is satisfied, false otherwise 184 | **--]] 185 | t.CFrame = t.typeof("CFrame") 186 | 187 | --[[** 188 | ensures Roblox Color3 type 189 | 190 | @param value The value to check against 191 | 192 | @returns True iff the condition is satisfied, false otherwise 193 | **--]] 194 | t.Color3 = t.typeof("Color3") 195 | 196 | --[[** 197 | ensures Roblox ColorSequence type 198 | 199 | @param value The value to check against 200 | 201 | @returns True iff the condition is satisfied, false otherwise 202 | **--]] 203 | t.ColorSequence = t.typeof("ColorSequence") 204 | 205 | --[[** 206 | ensures Roblox ColorSequenceKeypoint type 207 | 208 | @param value The value to check against 209 | 210 | @returns True iff the condition is satisfied, false otherwise 211 | **--]] 212 | t.ColorSequenceKeypoint = t.typeof("ColorSequenceKeypoint") 213 | 214 | --[[** 215 | ensures Roblox DateTime type 216 | 217 | @param value The value to check against 218 | 219 | @returns True iff the condition is satisfied, false otherwise 220 | **--]] 221 | t.DateTime = t.typeof("DateTime") 222 | 223 | --[[** 224 | ensures Roblox DockWidgetPluginGuiInfo type 225 | 226 | @param value The value to check against 227 | 228 | @returns True iff the condition is satisfied, false otherwise 229 | **--]] 230 | t.DockWidgetPluginGuiInfo = t.typeof("DockWidgetPluginGuiInfo") 231 | 232 | --[[** 233 | ensures Roblox Enum type 234 | 235 | @param value The value to check against 236 | 237 | @returns True iff the condition is satisfied, false otherwise 238 | **--]] 239 | t.Enum = t.typeof("Enum") 240 | 241 | --[[** 242 | ensures Roblox EnumItem type 243 | 244 | @param value The value to check against 245 | 246 | @returns True iff the condition is satisfied, false otherwise 247 | **--]] 248 | t.EnumItem = t.typeof("EnumItem") 249 | 250 | --[[** 251 | ensures Roblox Enums type 252 | 253 | @param value The value to check against 254 | 255 | @returns True iff the condition is satisfied, false otherwise 256 | **--]] 257 | t.Enums = t.typeof("Enums") 258 | 259 | --[[** 260 | ensures Roblox Faces type 261 | 262 | @param value The value to check against 263 | 264 | @returns True iff the condition is satisfied, false otherwise 265 | **--]] 266 | t.Faces = t.typeof("Faces") 267 | 268 | --[[** 269 | ensures Roblox FloatCurveKey type 270 | 271 | @param value The value to check against 272 | 273 | @returns True iff the condition is satisfied, false otherwise 274 | **--]] 275 | t.FloatCurveKey = t.typeof("FloatCurveKey") 276 | 277 | --[[** 278 | ensures Roblox Font type 279 | 280 | @param value The value to check against 281 | 282 | @returns True iff the condition is satisfied, false otherwise 283 | **--]] 284 | t.Font = t.typeof("Font") 285 | 286 | --[[** 287 | ensures Roblox Instance type 288 | 289 | @param value The value to check against 290 | 291 | @returns True iff the condition is satisfied, false otherwise 292 | **--]] 293 | t.Instance = t.typeof("Instance") 294 | 295 | --[[** 296 | ensures Roblox NumberRange type 297 | 298 | @param value The value to check against 299 | 300 | @returns True iff the condition is satisfied, false otherwise 301 | **--]] 302 | t.NumberRange = t.typeof("NumberRange") 303 | 304 | --[[** 305 | ensures Roblox NumberSequence type 306 | 307 | @param value The value to check against 308 | 309 | @returns True iff the condition is satisfied, false otherwise 310 | **--]] 311 | t.NumberSequence = t.typeof("NumberSequence") 312 | 313 | --[[** 314 | ensures Roblox NumberSequenceKeypoint type 315 | 316 | @param value The value to check against 317 | 318 | @returns True iff the condition is satisfied, false otherwise 319 | **--]] 320 | t.NumberSequenceKeypoint = t.typeof("NumberSequenceKeypoint") 321 | 322 | --[[** 323 | ensures Roblox OverlapParams type 324 | 325 | @param value The value to check against 326 | 327 | @returns True iff the condition is satisfied, false otherwise 328 | **--]] 329 | t.OverlapParams = t.typeof("OverlapParams") 330 | 331 | --[[** 332 | ensures Roblox PathWaypoint type 333 | 334 | @param value The value to check against 335 | 336 | @returns True iff the condition is satisfied, false otherwise 337 | **--]] 338 | t.PathWaypoint = t.typeof("PathWaypoint") 339 | 340 | --[[** 341 | ensures Roblox PhysicalProperties type 342 | 343 | @param value The value to check against 344 | 345 | @returns True iff the condition is satisfied, false otherwise 346 | **--]] 347 | t.PhysicalProperties = t.typeof("PhysicalProperties") 348 | 349 | --[[** 350 | ensures Roblox Random type 351 | 352 | @param value The value to check against 353 | 354 | @returns True iff the condition is satisfied, false otherwise 355 | **--]] 356 | t.Random = t.typeof("Random") 357 | 358 | --[[** 359 | ensures Roblox Ray type 360 | 361 | @param value The value to check against 362 | 363 | @returns True iff the condition is satisfied, false otherwise 364 | **--]] 365 | t.Ray = t.typeof("Ray") 366 | 367 | --[[** 368 | ensures Roblox RaycastParams type 369 | 370 | @param value The value to check against 371 | 372 | @returns True iff the condition is satisfied, false otherwise 373 | **--]] 374 | t.RaycastParams = t.typeof("RaycastParams") 375 | 376 | --[[** 377 | ensures Roblox RaycastResult type 378 | 379 | @param value The value to check against 380 | 381 | @returns True iff the condition is satisfied, false otherwise 382 | **--]] 383 | t.RaycastResult = t.typeof("RaycastResult") 384 | 385 | --[[** 386 | ensures Roblox RBXScriptConnection type 387 | 388 | @param value The value to check against 389 | 390 | @returns True iff the condition is satisfied, false otherwise 391 | **--]] 392 | t.RBXScriptConnection = t.typeof("RBXScriptConnection") 393 | 394 | --[[** 395 | ensures Roblox RBXScriptSignal type 396 | 397 | @param value The value to check against 398 | 399 | @returns True iff the condition is satisfied, false otherwise 400 | **--]] 401 | t.RBXScriptSignal = t.typeof("RBXScriptSignal") 402 | 403 | --[[** 404 | ensures Roblox Rect type 405 | 406 | @param value The value to check against 407 | 408 | @returns True iff the condition is satisfied, false otherwise 409 | **--]] 410 | t.Rect = t.typeof("Rect") 411 | 412 | --[[** 413 | ensures Roblox Region3 type 414 | 415 | @param value The value to check against 416 | 417 | @returns True iff the condition is satisfied, false otherwise 418 | **--]] 419 | t.Region3 = t.typeof("Region3") 420 | 421 | --[[** 422 | ensures Roblox Region3int16 type 423 | 424 | @param value The value to check against 425 | 426 | @returns True iff the condition is satisfied, false otherwise 427 | **--]] 428 | t.Region3int16 = t.typeof("Region3int16") 429 | 430 | --[[** 431 | ensures Roblox TweenInfo type 432 | 433 | @param value The value to check against 434 | 435 | @returns True iff the condition is satisfied, false otherwise 436 | **--]] 437 | t.TweenInfo = t.typeof("TweenInfo") 438 | 439 | --[[** 440 | ensures Roblox UDim type 441 | 442 | @param value The value to check against 443 | 444 | @returns True iff the condition is satisfied, false otherwise 445 | **--]] 446 | t.UDim = t.typeof("UDim") 447 | 448 | --[[** 449 | ensures Roblox UDim2 type 450 | 451 | @param value The value to check against 452 | 453 | @returns True iff the condition is satisfied, false otherwise 454 | **--]] 455 | t.UDim2 = t.typeof("UDim2") 456 | 457 | --[[** 458 | ensures Roblox Vector2 type 459 | 460 | @param value The value to check against 461 | 462 | @returns True iff the condition is satisfied, false otherwise 463 | **--]] 464 | t.Vector2 = t.typeof("Vector2") 465 | 466 | --[[** 467 | ensures Roblox Vector2int16 type 468 | 469 | @param value The value to check against 470 | 471 | @returns True iff the condition is satisfied, false otherwise 472 | **--]] 473 | t.Vector2int16 = t.typeof("Vector2int16") 474 | 475 | --[[** 476 | ensures Roblox Vector3 type 477 | 478 | @param value The value to check against 479 | 480 | @returns True iff the condition is satisfied, false otherwise 481 | **--]] 482 | t.Vector3 = t.typeof("Vector3") 483 | 484 | --[[** 485 | ensures Roblox Vector3int16 type 486 | 487 | @param value The value to check against 488 | 489 | @returns True iff the condition is satisfied, false otherwise 490 | **--]] 491 | t.Vector3int16 = t.typeof("Vector3int16") 492 | 493 | --[[** 494 | ensures value is a given literal value 495 | 496 | @param literal The literal to use 497 | 498 | @returns A function that will return true iff the condition is passed 499 | **--]] 500 | function t.literal(...) 501 | local size = select("#", ...) 502 | if size == 1 then 503 | local literal = ... 504 | return function(value) 505 | if value ~= literal then 506 | return false 507 | end 508 | 509 | return true 510 | end 511 | else 512 | local literals = {} 513 | for i = 1, size do 514 | local value = select(i, ...) 515 | literals[i] = t.literal(value) 516 | end 517 | 518 | return t.union(table.unpack(literals, 1, size)) 519 | end 520 | end 521 | 522 | --[[** 523 | DEPRECATED 524 | Please use t.literal 525 | **--]] 526 | t.exactly = t.literal 527 | 528 | --[[** 529 | Returns a t.union of each key in the table as a t.literal 530 | 531 | @param keyTable The table to get keys from 532 | 533 | @returns True iff the condition is satisfied, false otherwise 534 | **--]] 535 | function t.keyOf(keyTable) 536 | local keys = {} 537 | local length = 0 538 | for key in pairs(keyTable) do 539 | length = length + 1 540 | keys[length] = key 541 | end 542 | 543 | return t.literal(table.unpack(keys, 1, length)) 544 | end 545 | 546 | --[[** 547 | Returns a t.union of each value in the table as a t.literal 548 | 549 | @param valueTable The table to get values from 550 | 551 | @returns True iff the condition is satisfied, false otherwise 552 | **--]] 553 | function t.valueOf(valueTable) 554 | local values = {} 555 | local length = 0 556 | for _, value in pairs(valueTable) do 557 | length = length + 1 558 | values[length] = value 559 | end 560 | 561 | return t.literal(table.unpack(values, 1, length)) 562 | end 563 | 564 | --[[** 565 | ensures value is an integer 566 | 567 | @param value The value to check against 568 | 569 | @returns True iff the condition is satisfied, false otherwise 570 | **--]] 571 | function t.integer(value) 572 | local success = t.number(value) 573 | if not success then 574 | return false 575 | end 576 | 577 | if value % 1 == 0 then 578 | return true 579 | else 580 | return false 581 | end 582 | end 583 | 584 | --[[** 585 | ensures value is a number where min <= value 586 | 587 | @param min The minimum to use 588 | 589 | @returns A function that will return true iff the condition is passed 590 | **--]] 591 | function t.numberMin(min) 592 | return function(value) 593 | local success = t.number(value) 594 | if not success then 595 | return false 596 | end 597 | 598 | if value >= min then 599 | return true 600 | else 601 | return false 602 | end 603 | end 604 | end 605 | 606 | --[[** 607 | ensures value is a number where value <= max 608 | 609 | @param max The maximum to use 610 | 611 | @returns A function that will return true iff the condition is passed 612 | **--]] 613 | function t.numberMax(max) 614 | return function(value) 615 | local success = t.number(value) 616 | if not success then 617 | return false 618 | end 619 | 620 | if value <= max then 621 | return true 622 | else 623 | return false 624 | end 625 | end 626 | end 627 | 628 | --[[** 629 | ensures value is a number where min < value 630 | 631 | @param min The minimum to use 632 | 633 | @returns A function that will return true iff the condition is passed 634 | **--]] 635 | function t.numberMinExclusive(min) 636 | return function(value) 637 | local success = t.number(value) 638 | if not success then 639 | return false 640 | end 641 | 642 | if min < value then 643 | return true 644 | else 645 | return false 646 | end 647 | end 648 | end 649 | 650 | --[[** 651 | ensures value is a number where value < max 652 | 653 | @param max The maximum to use 654 | 655 | @returns A function that will return true iff the condition is passed 656 | **--]] 657 | function t.numberMaxExclusive(max) 658 | return function(value) 659 | local success = t.number(value) 660 | if not success then 661 | return false 662 | end 663 | 664 | if value < max then 665 | return true 666 | else 667 | return false 668 | end 669 | end 670 | end 671 | 672 | --[[** 673 | ensures value is a number where value > 0 674 | 675 | @returns A function that will return true iff the condition is passed 676 | **--]] 677 | t.numberPositive = t.numberMinExclusive(0) 678 | 679 | --[[** 680 | ensures value is a number where value < 0 681 | 682 | @returns A function that will return true iff the condition is passed 683 | **--]] 684 | t.numberNegative = t.numberMaxExclusive(0) 685 | 686 | --[[** 687 | ensures value is a number where min <= value <= max 688 | 689 | @param min The minimum to use 690 | @param max The maximum to use 691 | 692 | @returns A function that will return true iff the condition is passed 693 | **--]] 694 | function t.numberConstrained(min, max) 695 | assert(t.number(min)) 696 | assert(t.number(max)) 697 | local minCheck = t.numberMin(min) 698 | local maxCheck = t.numberMax(max) 699 | 700 | return function(value) 701 | local minSuccess = minCheck(value) 702 | if not minSuccess then 703 | return false 704 | end 705 | 706 | local maxSuccess = maxCheck(value) 707 | if not maxSuccess then 708 | return false 709 | end 710 | 711 | return true 712 | end 713 | end 714 | 715 | --[[** 716 | ensures value is a number where min < value < max 717 | 718 | @param min The minimum to use 719 | @param max The maximum to use 720 | 721 | @returns A function that will return true iff the condition is passed 722 | **--]] 723 | function t.numberConstrainedExclusive(min, max) 724 | assert(t.number(min)) 725 | assert(t.number(max)) 726 | local minCheck = t.numberMinExclusive(min) 727 | local maxCheck = t.numberMaxExclusive(max) 728 | 729 | return function(value) 730 | local minSuccess = minCheck(value) 731 | if not minSuccess then 732 | return false 733 | end 734 | 735 | local maxSuccess = maxCheck(value) 736 | if not maxSuccess then 737 | return false 738 | end 739 | 740 | return true 741 | end 742 | end 743 | 744 | --[[** 745 | ensures value matches string pattern 746 | 747 | @param string pattern to check against 748 | 749 | @returns A function that will return true iff the condition is passed 750 | **--]] 751 | function t.match(pattern) 752 | assert(t.string(pattern)) 753 | return function(value) 754 | local stringSuccess = t.string(value) 755 | if not stringSuccess then 756 | return false 757 | end 758 | 759 | if string.match(value, pattern) == nil then 760 | return false 761 | end 762 | 763 | return true 764 | end 765 | end 766 | 767 | --[[** 768 | ensures value is either nil or passes check 769 | 770 | @param check The check to use 771 | 772 | @returns A function that will return true iff the condition is passed 773 | **--]] 774 | function t.optional(check) 775 | assert(t.callback(check)) 776 | return function(value) 777 | if value == nil then 778 | return true 779 | end 780 | 781 | local success = check(value) 782 | if success then 783 | return true 784 | else 785 | return false 786 | end 787 | end 788 | end 789 | 790 | --[[** 791 | matches given tuple against tuple type definition 792 | 793 | @param ... The type definition for the tuples 794 | 795 | @returns A function that will return true iff the condition is passed 796 | **--]] 797 | function t.tuple(...) 798 | local checks = { ... } 799 | return function(...) 800 | local args = { ... } 801 | for i, check in ipairs(checks) do 802 | local success = check(args[i]) 803 | if success == false then 804 | return false 805 | end 806 | end 807 | 808 | return true 809 | end 810 | end 811 | 812 | --[[** 813 | ensures all keys in given table pass check 814 | 815 | @param check The function to use to check the keys 816 | 817 | @returns A function that will return true iff the condition is passed 818 | **--]] 819 | function t.keys(check) 820 | assert(t.callback(check)) 821 | return function(value) 822 | local tableSuccess = t.table(value) 823 | if tableSuccess == false then 824 | return false 825 | end 826 | 827 | for key in pairs(value) do 828 | local success = check(key) 829 | if success == false then 830 | return false 831 | end 832 | end 833 | 834 | return true 835 | end 836 | end 837 | 838 | --[[** 839 | ensures all values in given table pass check 840 | 841 | @param check The function to use to check the values 842 | 843 | @returns A function that will return true iff the condition is passed 844 | **--]] 845 | function t.values(check) 846 | assert(t.callback(check)) 847 | return function(value) 848 | local tableSuccess = t.table(value) 849 | if tableSuccess == false then 850 | return false 851 | end 852 | 853 | for _, val in pairs(value) do 854 | local success = check(val) 855 | if success == false then 856 | return false 857 | end 858 | end 859 | 860 | return true 861 | end 862 | end 863 | 864 | --[[** 865 | ensures value is a table and all keys pass keyCheck and all values pass valueCheck 866 | 867 | @param keyCheck The function to use to check the keys 868 | @param valueCheck The function to use to check the values 869 | 870 | @returns A function that will return true iff the condition is passed 871 | **--]] 872 | function t.map(keyCheck, valueCheck) 873 | assert(t.callback(keyCheck)) 874 | assert(t.callback(valueCheck)) 875 | local keyChecker = t.keys(keyCheck) 876 | local valueChecker = t.values(valueCheck) 877 | 878 | return function(value) 879 | local keySuccess = keyChecker(value) 880 | if not keySuccess then 881 | return false 882 | end 883 | 884 | local valueSuccess = valueChecker(value) 885 | if not valueSuccess then 886 | return false 887 | end 888 | 889 | return true 890 | end 891 | end 892 | 893 | --[[** 894 | ensures value is a table and all keys pass valueCheck and all values are true 895 | 896 | @param valueCheck The function to use to check the values 897 | 898 | @returns A function that will return true iff the condition is passed 899 | **--]] 900 | function t.set(valueCheck) 901 | return t.map(valueCheck, t.literal(true)) 902 | end 903 | 904 | do 905 | local arrayKeysCheck = t.keys(t.integer) 906 | --[[** 907 | ensures value is an array and all values of the array match check 908 | 909 | @param check The check to compare all values with 910 | 911 | @returns A function that will return true iff the condition is passed 912 | **--]] 913 | function t.array(check) 914 | assert(t.callback(check)) 915 | local valuesCheck = t.values(check) 916 | 917 | return function(value) 918 | local keySuccess = arrayKeysCheck(value) 919 | if keySuccess == false then 920 | return false 921 | end 922 | 923 | -- # is unreliable for sparse arrays 924 | -- Count upwards using ipairs to avoid false positives from the behavior of # 925 | local arraySize = 0 926 | 927 | for _ in ipairs(value) do 928 | arraySize = arraySize + 1 929 | end 930 | 931 | for key in pairs(value) do 932 | if key < 1 or key > arraySize then 933 | return false 934 | end 935 | end 936 | 937 | local valueSuccess = valuesCheck(value) 938 | if not valueSuccess then 939 | return false 940 | end 941 | 942 | return true 943 | end 944 | end 945 | 946 | --[[** 947 | ensures value is an array of a strict makeup and size 948 | 949 | @param check The check to compare all values with 950 | 951 | @returns A function that will return true iff the condition is passed 952 | **--]] 953 | function t.strictArray(...) 954 | local valueTypes = { ... } 955 | assert(t.array(t.callback)(valueTypes)) 956 | 957 | return function(value) 958 | local keySuccess = arrayKeysCheck(value) 959 | if keySuccess == false then 960 | return false 961 | end 962 | 963 | -- If there's more than the set array size, disallow 964 | if #valueTypes < #value then 965 | return false 966 | end 967 | 968 | for idx, typeFn in pairs(valueTypes) do 969 | local typeSuccess = typeFn(value[idx]) 970 | if not typeSuccess then 971 | return false 972 | end 973 | end 974 | 975 | return true 976 | end 977 | end 978 | end 979 | 980 | do 981 | local callbackArray = t.array(t.callback) 982 | --[[** 983 | creates a union type 984 | 985 | @param ... The checks to union 986 | 987 | @returns A function that will return true iff the condition is passed 988 | **--]] 989 | function t.union(...) 990 | local checks = { ... } 991 | assert(callbackArray(checks)) 992 | 993 | return function(value) 994 | for _, check in ipairs(checks) do 995 | if check(value) then 996 | return true 997 | end 998 | end 999 | 1000 | return false 1001 | end 1002 | end 1003 | 1004 | --[[** 1005 | Alias for t.union 1006 | **--]] 1007 | t.some = t.union 1008 | 1009 | --[[** 1010 | creates an intersection type 1011 | 1012 | @param ... The checks to intersect 1013 | 1014 | @returns A function that will return true iff the condition is passed 1015 | **--]] 1016 | function t.intersection(...) 1017 | local checks = { ... } 1018 | assert(callbackArray(checks)) 1019 | 1020 | return function(value) 1021 | for _, check in ipairs(checks) do 1022 | local success = check(value) 1023 | if not success then 1024 | return false 1025 | end 1026 | end 1027 | 1028 | return true 1029 | end 1030 | end 1031 | 1032 | --[[** 1033 | Alias for t.intersection 1034 | **--]] 1035 | t.every = t.intersection 1036 | end 1037 | 1038 | do 1039 | local checkInterface = t.map(t.any, t.callback) 1040 | --[[** 1041 | ensures value matches given interface definition 1042 | 1043 | @param checkTable The interface definition 1044 | 1045 | @returns A function that will return true iff the condition is passed 1046 | **--]] 1047 | function t.interface(checkTable) 1048 | assert(checkInterface(checkTable)) 1049 | return function(value) 1050 | local tableSuccess = t.table(value) 1051 | if tableSuccess == false then 1052 | return false 1053 | end 1054 | 1055 | for key, check in pairs(checkTable) do 1056 | local success = check(value[key]) 1057 | if success == false then 1058 | return false 1059 | end 1060 | end 1061 | 1062 | return true 1063 | end 1064 | end 1065 | 1066 | --[[** 1067 | ensures value matches given interface definition strictly 1068 | 1069 | @param checkTable The interface definition 1070 | 1071 | @returns A function that will return true iff the condition is passed 1072 | **--]] 1073 | function t.strictInterface(checkTable) 1074 | assert(checkInterface(checkTable)) 1075 | return function(value) 1076 | local tableSuccess = t.table(value) 1077 | if tableSuccess == false then 1078 | return false 1079 | end 1080 | 1081 | for key, check in pairs(checkTable) do 1082 | local success = check(value[key]) 1083 | if success == false then 1084 | return false 1085 | end 1086 | end 1087 | 1088 | for key in pairs(value) do 1089 | if not checkTable[key] then 1090 | return false 1091 | end 1092 | end 1093 | 1094 | return true 1095 | end 1096 | end 1097 | end 1098 | 1099 | --[[** 1100 | ensure value is an Instance and it's ClassName matches the given ClassName 1101 | 1102 | @param className The class name to check for 1103 | 1104 | @returns A function that will return true iff the condition is passed 1105 | **--]] 1106 | function t.instanceOf(className, childTable) 1107 | assert(t.string(className)) 1108 | 1109 | local childrenCheck 1110 | if childTable ~= nil then 1111 | childrenCheck = t.children(childTable) 1112 | end 1113 | 1114 | return function(value) 1115 | local instanceSuccess = t.Instance(value) 1116 | if not instanceSuccess then 1117 | return false 1118 | end 1119 | 1120 | if value.ClassName ~= className then 1121 | return false 1122 | end 1123 | 1124 | if childrenCheck then 1125 | local childrenSuccess = childrenCheck(value) 1126 | if not childrenSuccess then 1127 | return false 1128 | end 1129 | end 1130 | 1131 | return true 1132 | end 1133 | end 1134 | 1135 | t.instance = t.instanceOf 1136 | 1137 | --[[** 1138 | ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison 1139 | 1140 | @param className The class name to check for 1141 | 1142 | @returns A function that will return true iff the condition is passed 1143 | **--]] 1144 | function t.instanceIsA(className, childTable) 1145 | assert(t.string(className)) 1146 | 1147 | local childrenCheck 1148 | if childTable ~= nil then 1149 | childrenCheck = t.children(childTable) 1150 | end 1151 | 1152 | return function(value) 1153 | local instanceSuccess = t.Instance(value) 1154 | if not instanceSuccess then 1155 | return false 1156 | end 1157 | 1158 | if not value:IsA(className) then 1159 | return false 1160 | end 1161 | 1162 | if childrenCheck then 1163 | local childrenSuccess = childrenCheck(value) 1164 | if not childrenSuccess then 1165 | return false 1166 | end 1167 | end 1168 | 1169 | return true 1170 | end 1171 | end 1172 | 1173 | --[[** 1174 | ensures value is an enum of the correct type 1175 | 1176 | @param enum The enum to check 1177 | 1178 | @returns A function that will return true iff the condition is passed 1179 | **--]] 1180 | function t.enum(enum) 1181 | assert(t.Enum(enum)) 1182 | return function(value) 1183 | local enumItemSuccess = t.EnumItem(value) 1184 | if not enumItemSuccess then 1185 | return false 1186 | end 1187 | 1188 | if value.EnumType == enum then 1189 | return true 1190 | else 1191 | return false 1192 | end 1193 | end 1194 | end 1195 | 1196 | do 1197 | local checkWrap = t.tuple(t.callback, t.callback) 1198 | 1199 | --[[** 1200 | wraps a callback in an assert with checkArgs 1201 | 1202 | @param callback The function to wrap 1203 | @param checkArgs The function to use to check arguments in the assert 1204 | 1205 | @returns A function that first asserts using checkArgs and then calls callback 1206 | **--]] 1207 | function t.wrap(callback, checkArgs) 1208 | assert(checkWrap(callback, checkArgs)) 1209 | return function(...) 1210 | assert(checkArgs(...)) 1211 | return callback(...) 1212 | end 1213 | end 1214 | end 1215 | 1216 | --[[** 1217 | asserts a given check 1218 | 1219 | @param check The function to wrap with an assert 1220 | 1221 | @returns A function that simply wraps the given check in an assert 1222 | **--]] 1223 | function t.strict(check) 1224 | return function(...) 1225 | assert(check(...)) 1226 | end 1227 | end 1228 | 1229 | do 1230 | local checkChildren = t.map(t.string, t.callback) 1231 | 1232 | --[[** 1233 | Takes a table where keys are child names and values are functions to check the children against. 1234 | Pass an instance tree into the function. 1235 | If at least one child passes each check, the overall check passes. 1236 | 1237 | Warning! If you pass in a tree with more than one child of the same name, this function will always return false 1238 | 1239 | @param checkTable The table to check against 1240 | 1241 | @returns A function that checks an instance tree 1242 | **--]] 1243 | function t.children(checkTable) 1244 | assert(checkChildren(checkTable)) 1245 | 1246 | return function(value) 1247 | local instanceSuccess = t.Instance(value) 1248 | if not instanceSuccess then 1249 | return false 1250 | end 1251 | 1252 | local childrenByName = {} 1253 | for _, child in ipairs(value:GetChildren()) do 1254 | local name = child.Name 1255 | if checkTable[name] then 1256 | if childrenByName[name] then 1257 | return false 1258 | end 1259 | 1260 | childrenByName[name] = child 1261 | end 1262 | end 1263 | 1264 | for name, check in pairs(checkTable) do 1265 | local success = check(childrenByName[name]) 1266 | if not success then 1267 | return false 1268 | end 1269 | end 1270 | 1271 | return true 1272 | end 1273 | end 1274 | end 1275 | 1276 | return { t = t } 1277 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/t", 3 | "version": "3.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@rbxts/t", 9 | "version": "3.1.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@rbxts/compiler-types": "^2.0.4-types.1", 13 | "@rbxts/types": "^1.0.654" 14 | } 15 | }, 16 | "node_modules/@rbxts/compiler-types": { 17 | "version": "2.0.4-types.1", 18 | "resolved": "https://registry.npmjs.org/@rbxts/compiler-types/-/compiler-types-2.0.4-types.1.tgz", 19 | "integrity": "sha512-sLFiyMH9j8kcCd07lt6R+WgneksCGqYUFKu7qX02ufUU7r9eR2dXoOt2A9Nxh3unTuCu5mYyO0M6JYb82fOHxA==", 20 | "dev": true 21 | }, 22 | "node_modules/@rbxts/types": { 23 | "version": "1.0.654", 24 | "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.654.tgz", 25 | "integrity": "sha512-rro8POwbsLenr4KJOrfFTYlS85BWOu0fgh2+hz/qI4bDCbCanQNMO1+8ZUlktXvQQqIqUzGFx15h8edZT/LLIg==", 26 | "dev": true 27 | } 28 | }, 29 | "dependencies": { 30 | "@rbxts/compiler-types": { 31 | "version": "2.0.4-types.1", 32 | "resolved": "https://registry.npmjs.org/@rbxts/compiler-types/-/compiler-types-2.0.4-types.1.tgz", 33 | "integrity": "sha512-sLFiyMH9j8kcCd07lt6R+WgneksCGqYUFKu7qX02ufUU7r9eR2dXoOt2A9Nxh3unTuCu5mYyO0M6JYb82fOHxA==", 34 | "dev": true 35 | }, 36 | "@rbxts/types": { 37 | "version": "1.0.654", 38 | "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.654.tgz", 39 | "integrity": "sha512-rro8POwbsLenr4KJOrfFTYlS85BWOu0fgh2+hz/qI4bDCbCanQNMO1+8ZUlktXvQQqIqUzGFx15h8edZT/LLIg==", 40 | "dev": true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/t", 3 | "version": "3.1.0", 4 | "description": "A Runtime Type Checker for Roblox", 5 | "main": "lib/ts.lua", 6 | "types": "lib/t.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/osyrisrblx/t.git" 10 | }, 11 | "keywords": [], 12 | "author": "osyris", 13 | "license": "ISC", 14 | "bugs": { 15 | "url": "https://github.com/osyrisrblx/t/issues" 16 | }, 17 | "homepage": "https://github.com/osyrisrblx/t#readme", 18 | "devDependencies": { 19 | "@rbxts/compiler-types": "^2.0.4-types.1", 20 | "@rbxts/types": "^1.0.654" 21 | }, 22 | "files": [ 23 | "lib/ts.lua", 24 | "lib/t.d.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /rotriever.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "t" 3 | authors = [ 4 | "osyrisrblx" 5 | ] 6 | content_root = "lib" 7 | version = "3.0.0" 8 | license = "MIT" 9 | files = [ "init.lua" ] -------------------------------------------------------------------------------- /spec.lua: -------------------------------------------------------------------------------- 1 | -- polyfills 2 | table.unpack = unpack -- luacheck: ignore 3 | 4 | -- borrowed from Roact 5 | 6 | local LOAD_MODULES = { 7 | Library = "lib", 8 | TestEZ = "modules/testez/src", 9 | } 10 | 11 | package.path = package.path .. ";?/init.lua" 12 | local lemur = require("modules.lemur") 13 | local habitat = lemur.Habitat.new() 14 | 15 | local Root = lemur.Instance.new("Folder") 16 | Root.Name = "Root" 17 | 18 | for name, path in pairs(LOAD_MODULES) do 19 | local container = habitat:loadFromFs(path) 20 | container.Name = name 21 | container.Parent = Root 22 | end 23 | 24 | local TestEZ = habitat:require(Root.TestEZ) 25 | local results = TestEZ.TestBootstrap:run({ Root.Library }, TestEZ.Reporters.TextReporter) 26 | 27 | if results.failureCount > 0 then 28 | os.exit(1) 29 | end 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "isolatedModules": true, 5 | "downlevelIteration": true, 6 | "module": "commonjs", 7 | "noLib": true, 8 | "strict": true, 9 | "target": "es6", 10 | "typeRoots": [ "node_modules/@rbxts" ], 11 | "baseUrl": ".", 12 | "declaration": false, 13 | "jsx": "react", 14 | "jsxFactory": "Roact.createElement" 15 | } 16 | } -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "osyrisrblx/t" 3 | description = "A Runtime Typechecker for Roblox" 4 | version = "3.0.0" 5 | license = "MIT" 6 | authors = ["Osyris"] 7 | realm = "shared" 8 | registry = "https://github.com/upliftgames/wally-index" 9 | exclude = ["*"] 10 | include = ["lib", "lib/init.lua", "default.project.json", "LICENSE", "README.md", "wally.toml"] 11 | 12 | [dependencies] 13 | --------------------------------------------------------------------------------