Skip to content

Instantly share code, notes, and snippets.

@cameronpcampbell
Last active June 5, 2025 19:51
Show Gist options
  • Save cameronpcampbell/0a491ec5c4f2143e00b775f610424fd6 to your computer and use it in GitHub Desktop.
Save cameronpcampbell/0a491ec5c4f2143e00b775f610424fd6 to your computer and use it in GitHub Desktop.
--!strict
--!nolint LocalShadow
--> Table Helpers ----------------------------------------------------------------------
--[=[
Performs a swap remove on a table type.
@category Table
@param input { [any]: any } -- The table value to perform a swap remove on.
@param idx number -- The index to swap remove.
@param input_len number? -- You can optionally pass the length of the input.
@example
local tble = { 1, 2, 3, 4 }
table_value_swap_remove(tble, 2)
-- tble = { 1, 4, 3 }
]=]
type function table_value_swap_remove(input: { [any]: any }, idx: number, input_len: number?)
local input_len = input_len or #input
-- Last item, we can just remove from
-- the end of the input table.
if idx == input_len then
table.remove(input, idx)
-- Swap remove optimisation.
else
input[idx] = table.remove(input)
end
return input_len - 1
end
----------------------------------------------------------------------------------------
--> Negation Helpers -------------------------------------------------------------------
--[=[
Creates a negation with a specified amount of nestedness
@category Negation
@param input any -- The type to be negated.
@param amount any -- The amount of nestedness.
@example
local result = negation_nested(string, 3)
-- local result = ~~~string
]=]
type function negation_nested(input: type, amount: number)
for count = 1, amount do
input = types.negationof(input)
end
return input
end
----------------------------------------------------------------------------------------
--> Inner Helpers ----------------------------------------------------------------------
--[=[
Gets the inner tag of a type. Recurses until it finds a non-negated type.
@category Inner
@param input any -- The type to get the inner tag of.
@example
local result = inner_tag_deep(types.negationof(types.negationof(types.negationof(types.string))))
-- local result = string
]=]
type function inner_tag_deep(input: type)
local input_tag = input.tag
return if input_tag == "negation" then inner_tag_deep(input:inner()).tag else input_tag
end
--[=[
Maps the innermost (recurses until it finds a non-negated type) value of a type.
@category Inner
@param input any -- The type to map the innermost value of.
@param callback any -- The function which maps the innermost value.
@example
local result = inner_map_deep(types.negationof(types.negationof(types.string)), function(inner: type, inner_tag: string)
return if inner_tag == "string" then types.number else inner
end)
-- local result = ~~number
]=]
type function inner_map_deep(input: type, callback: (inner: type, input_tag: string) -> type)
local input_tag = input.tag
if input_tag == "negation" then
local negation_nestedness = 0
while true do
input = input:inner()
input_tag = input.tag
negation_nestedness += 1
if input_tag ~= "negation" then break end
end
return negation_nested(callback(input, input_tag), negation_nestedness)
else
return callback(input, input_tag)
end
end
----------------------------------------------------------------------------------------
--> Components Helpers -----------------------------------------------------------------
--[=[
Recursively filters a components table. A much better developer experience is provided via the `union_filter_deep` and `intersection_filter_deep` type functions. If every component in the table is filtered then `nil` is returned.
@category Component
@param input { [number]: type } -- the components table to filter.
@param as "union" | "intersection" -- The type tag which the components table belongs to.
@param as_builder (components: { [number]: type }) -> (typeof(types.unionof()) | typeof(types.intersectionof())) -- A function to turn inner component tables back into a type.
@param callback (component: type) -> boolean -- The function which filters the components. Return `true` to filter a component.
@example
local result = components_filter_deep(
types.unionof(types.string, types.unionof(types.number, types.boolean)):components(),
"union",
union_from_components,
function(component: type)
return component == types.string
end
)
-- local result = { boolean, number }
]=]
type function components_filter_deep(
input: { [number]: type },
as: "union" | "intersection",
as_builder: (components: { [number]: type }) -> (typeof(types.unionof()) | typeof(types.intersectionof())),
callback: (component: type) -> boolean
)
local input_len = #input
for idx = input_len, 1, -1 do
local component = input[idx]
if component.tag == as then
local filtered = as_builder(components_filter_deep(component:components(), as, as_builder, callback))
if filtered == nil then
input_len = table_value_swap_remove(input, idx, input_len)
else
input[idx] = filtered
end
else
if not callback(component) then continue end
input_len = table_value_swap_remove(input, idx, input_len)
end
end
return input
end
--[=[
Maps a component table. A much better developer experience is provided via the `union_map_shallow` and `intersection_map_shallow` type functions.
@category Component
@param input { [number]: type } -- the components table to map.
@param callback (component: type) -> boolean -- The function which maps the components.
@example
local result = components_map_shallow(
types.unionof(types.string, types.unionof(types.number, types.boolean)):components(),
function(component: type)
return if component == types.string then types.singleton("mapped") else component
end
)
-- local result = { "mapped", (boolean | number) }
]=]
type function components_map_shallow(
input: { [number]: type },
callback: (value: type, ...any) -> any
)
for idx, value in input do
input[idx] = callback(value)
end
return input
end
--[=[
Recursively maps a component table. A much better developer experience is provided via the `union_map_deep` and `intersection_map_deep` type functions.
@category Component
@param input { [number]: type } -- the components table to map.
@param as "union" | "intersection" -- The type tag which the components table belongs to.
@param as_builder (components: { [number]: type }) -> (typeof(types.unionof()) | typeof(types.intersectionof())) -- A function to turn inner component tables back into a type.
@param callback (component: type) -> boolean -- The function which maps the components.
@example
local result = components_map_deep(
types.unionof(types.string, types.unionof(types.number, types.boolean)):components(),
"union",
union_from_components,
function(component: type)
return if component == types.number then types.singleton("mapped") else component
end
)
-- local result = { ("mapped" | boolean), string }
]=]
type function components_map_deep(
input: { [number]: type },
as: "union" | "intersection",
as_builder: (components: { [number]: type }) -> (typeof(types.unionof()) | typeof(types.intersectionof())),
callback: (value: type, ...any) -> any
)
for idx, value in input do
input[idx] = if value.tag == as then as_builder(components_map_deep(value:components(), as, as_builder, callback)) else callback(value)
end
return input
end
----------------------------------------------------------------------------------------
--> Union Helpers ----------------------------------------------------------------------
--[=[
Turns a component table into a union. If the table has only one component then it returns that component, and if the component table is empty then it returns nil.
@category Component
@param input { [number]: type } -- the components table to turn into a union.
@example
local result = union_from_components({ types.string, types.number, types.boolean })
-- local result = boolean | number | string
]=]
type function union_from_components(input: { [number]: type })
local input_len = #input
return if input_len == 0 then nil elseif input_len == 1 then input[1] else types.unionof(table.unpack(input))
end
--[=[
Recursively filters a union (passing in a non-union will be treated as a single component). If every component in the union is filtered then `nil` is returned.
@category Component
@param input type -- the union to filter.
@param callback (component: type) -> boolean -- The function which filters the components. Return `true` to filter a component.
@example
local result = union_filter_deep(
types.unionof(types.string, types.unionof(types.number, types.boolean)),
function(component: type)
return component == types.string
end
) or types.never
-- local result = boolean | number
]=]
type function union_filter_deep(input: type, callback: (component: type) -> boolean)
if input.tag == "union" then
return union_from_components(components_filter_deep(input:components(), "union", union_from_components, callback))
else
return if callback(input) then nil else input
end
end
--[=[
Maps a union.
@category Component
@param input type -- the union table to map.
@param callback (component: type) -> boolean -- The function which maps the unions components.
@example
local result = union_map_shallow(
types.unionof(types.string, types.unionof(types.number, types.boolean)):components(),
function(component: type)
return if component == types.string then types.singleton("mapped") else component
end
)
-- local result = "mapped" | (boolean | number)
]=]
type function union_map_shallow(input: type, callback: (component: type) -> boolean)
if input.tag == "union" then
return union_from_components(components_map_shallow(input:components(), callback))
else
return callback(input)
end
end
--[=[
Recursively maps a union.
@category Component
@param input { [number]: type } -- the union to recursively map.
@param callback (component: type) -> boolean -- The function which maps the unions components.
@example
local result = union_map_deep(
types.unionof(types.string, types.unionof(types.number, types.boolean)):components(),
function(component: type)
return if component == types.number then types.singleton("mapped") else component
end
)
-- local result = ("mapped" | boolean) | string
]=]
type function union_map_deep(input: type, callback: (component: type) -> boolean)
if input.tag == "union" then
return union_from_components(components_map_deep(input:components(), "union", union_from_components, callback))
else
return callback(input)
end
end
----------------------------------------------------------------------------------------
--> Intersection Helpers ---------------------------------------------------------------
--[=[
Turns a component table into an intersection. If the table has only one component then it returns that component, and if the component table is empty then it returns nil.
@category Component
@param input { [number]: type } -- the components table to turn into an intersection.
@example
local result = intersection_from_components({ types.string, types.number, types.boolean })
-- local result = boolean & number & string
]=]
type function intersection_from_components(input: { [number]: type })
local input_len = #input
return if input_len == 0 then nil elseif input_len == 1 then input[1] else types.intersectionof(table.unpack(input))
end
--[=[
Recursively filters an intersection (passing in a non-intersection will be treated as a single component). If every component in the intersection is filtered then `nil` is returned.
@category Component
@param input type -- the intersection to filter.
@param callback (component: type) -> boolean -- The function which filters the components. Return `true` to filter a component.
@example
local result = intersection_filter_deep(
types.intersectionof(types.string, types.intersectionof(types.number, types.boolean)),
function(component: type)
return component == types.string
end
) or types.never
-- local result = boolean & number
]=]
type function intersection_filter_deep(input: type, callback: (component: type) -> boolean)
if input.tag == "intersection" then
return intersection_from_components(components_filter_deep(input:components(), "intersection", intersection_from_components, callback))
else
return if callback(input) then nil else input
end
end
--[=[
Maps an intersection.
@category Component
@param input type -- the intersection table to map.
@param callback (component: type) -> boolean -- The function which maps the intersections components.
@example
local result = intersection_map_shallow(
types.intersectionof(types.string, types.intersectionof(types.number, types.boolean)):components(),
function(component: type)
return if component == types.string then types.singleton("mapped") else component
end
)
-- local result = "mapped" & (boolean & number)
]=]
type function intersection_map_shallow(input: type, input_tag: string, callback: (component: type) -> boolean)
if input_tag == "intersection" then
return intersection_from_components(components_map_shallow(input:components(), callback))
else
return callback(input)
end
end
--[=[
Recursively maps an intersection.
@category Component
@param input { [number]: type } -- the intersection to recursively map.
@param callback (component: type) -> boolean -- The function which maps the intersections components.
@example
local result = intersection_map_deep(
types.intersectionof(types.string, types.intersectionof(types.number, types.boolean)):components(),
function(component: type)
return if component == types.number then types.singleton("mapped") else component
end
)
-- local result = ("mapped" & boolean) & string
]=]
type function intersection_map_deep(input: type, input_tag: string, callback: (component: type) -> boolean)
if input_tag == "intersection" then
return intersection_from_components(components_map_deep(input:components(), "intersection", intersection_from_components, callback))
else
return callback(input)
end
end
----------------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment