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