Skip to content
This repository has been archived by the owner on Dec 13, 2023. It is now read-only.

Add Type Checking API #230

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased Changes
* Improved the error message when an invalid changed hook name is used. ([#216](https://github.com/Roblox/roact/pull/216))
* Fixed a bug where fragments could not be used as children of an element or another fragment. ([#214](https://github.com/Roblox/roact/pull/214))
* Added Roact.Type and Roact.typeOf for Roact object type checking. ([#230](https://github.com/Roblox/roact/pull/230))

## [1.1.0](https://github.com/Roblox/roact/releases/tag/v1.1.0) (June 3rd, 2019)
* Fixed an issue where updating a host element with children to an element with `nil` children caused the old children to not be unmounted. ([#210](https://github.com/Roblox/roact/pull/210))
Expand Down
71 changes: 71 additions & 0 deletions docs/advanced/inspecting-roact-objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
In certain situations, such as when building highly reusable and customizable components, props may be composed of Roact objects, such as an element or a component class.

To facilitate safer development for these kinds of situations, Roact exposes the `Roact.typeOf` function to inspect Roact objects and return a value from the `Roact.Type` enumeration.

## Without Object Type Inspection

Suppose we want to write a Header component with a prop for the title child element:
```lua
local Header = Component:extend("Header")
function Header:render()
local titleClass = props.titleClass
return Roact.createElement("Frame", {
-- Props for Frame...
}, {
Title = Roact.createElement(titleClass, {
-- Props for Title...
})
})
end
```

Now suppose we want to validate that titleClass is actually a class using [validateProps](../../api-reference/#validateprops). Unfortunately, the best we can do is query Header to see if it contains characteristics of a Component class:
```lua
local Header = Component:extend("Header")
Header.validateProps = function()
local titleClass = props.titleClass
if type(titleClass.render) == "function" then
return true
end
return false, tostring(Header) .. " prop titleClass cannot render"
end
```

## With Object Type Inspection

With `Roact.typeOf`, we can be certain we have a Component class:
```lua
Header.validateProps = function()
local titleClass = props.titleClass
if Roact.typeOf(titleClass) == Roact.Type.StatefulComponentClass then
MisterUncloaked marked this conversation as resolved.
Show resolved Hide resolved
return true
end
return false, tostring(Header) .. " prop titleClass is not a component class"
end
```

We can even provide props which can be of multiple different Roact object types to give the consumer more flexibility:
```lua
local Header = Component:extend("Header")
Header.validateProps = function()
local title = props.title -- Type.Element | Type.StatefulComponentClass
local isElement = Roact.typeOf(title) == Roact.Type.Element
local isClass = Roact.typeOf(title) == Roact.Type.StatefulComponentClass
if isElement or isClass then
return true
end
return false, tostring(Header) .. " prop title must be a class or element"
end
function Header:render()
local title = props.title
local isElement = Roact.typeOf(title) == Roact.Type.Element
local isClass = Roact.typeOf(title) == Roact.Type.StatefulComponentClass
return Roact.createElement("Frame", {
-- Props for Frame...
}, {
Title = isElement and title or isClass and Roact.createElement(title, {
-- Props for Title...
})
})
end
```
49 changes: 49 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ local function Flex()
end
```

---

### Roact.typeOf
<div class="api-addition">Added in 1.2.0</div>



---

### Roact.createRef
Expand Down Expand Up @@ -356,6 +363,48 @@ See [the Portals guide](../advanced/portals) for a small tutorial and more detai

---

## Enumerations

### Roact.Type
<div class="api-addition">Added in 1.2.0</div>

An enumeration of the various types of objects in Roact, returned from calling `Roact.typeOf` on Roact objects.

#### Roact.Type.Binding
`Roact.typeOf` object returned from `Roact.createBinding`

---

#### Roact.Type.Element
`Roact.typeOf` object returned from `Roact.createElement`

---

#### Roact.Type.HostChangeEvent
`Roact.typeOf` object returned when indexing into `Roact.Change`

---

#### Roact.Type.HostEvent
`Roact.typeOf` object returned when indexing into `Roact.Event`

---

#### Roact.Type.StatefulComponentClass
`Roact.typeOf` object returned from `Roact.Component:extend`

---

#### Roact.Type.StatefulComponentInstance
`Roact.typeOf` object of self inside of member methods of `Roact.Component`

---

#### Roact.Type.VirtualTree
`Roact.typeOf` object returned by `Roact.mount`

---

## Component API

### defaultProps
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ nav:
- Portals: advanced/portals.md
- Bindings and Refs: advanced/bindings-and-refs.md
- Context: advanced/context.md
- Inspecting Roact Objects: advanced/inspecting-roact-objects.md
- Performance Optimization:
- Overview: performance/overview.md
- Reduce Reconcilation: performance/reduce-reconciliation.md
Expand Down
14 changes: 13 additions & 1 deletion src/Type.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ local strict = require(script.Parent.strict)
local Type = newproxy(true)

local TypeInternal = {}
local TypeNames = {}

local function addType(name)
TypeInternal[name] = Symbol.named("Roact" .. name)
local symbol = Symbol.named("Roact" .. name)
TypeNames[symbol] = name
TypeInternal[name] = symbol
end

addType("Binding")
Expand All @@ -37,12 +40,21 @@ function TypeInternal.of(value)
return value[Type]
end

function TypeInternal.nameOf(type)
if typeof(type) ~= "userdata" then
return nil
end
MisterUncloaked marked this conversation as resolved.
Show resolved Hide resolved

return TypeNames[type]
end

getmetatable(Type).__index = TypeInternal

getmetatable(Type).__tostring = function()
return "RoactType"
end

strict(TypeInternal, "Type")
strict(TypeNames, "TypeNames")

return Type
4 changes: 4 additions & 0 deletions src/Type.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ return function()

expect(Type.of(test)).to.equal(Type.Element)
end)

it("should return a type's name", function()
expect(Type.nameOf(Type.Element)).to.equal("Element")
end)
end)
end
58 changes: 58 additions & 0 deletions src/TypeMirror.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
--[[
Mirrors a subset of values from Type.lua for external use, allowing
type checking on Roact objects without exposing internal Type symbols

TypeMirror: {
Type: Roact.Type,
typeof: function(value: table) -> Roact.Type | nil
MisterUncloaked marked this conversation as resolved.
Show resolved Hide resolved
}
]]

local Type = require(script.Parent.Type)
local Symbol = require(script.Parent.Symbol)
local strict = require(script.Parent.strict)

local ALLOWED_TYPES = {
Type.Binding,
Type.Element,
Type.HostChangeEvent,
Type.HostEvent,
Type.StatefulComponentClass,
Type.StatefulComponentInstance,
Type.VirtualTree
}

local MirroredType = newproxy(true)
local MirroredTypeInternal = {}
for _, type in ipairs(ALLOWED_TYPES) do
local name = Type.nameOf(type)
MirroredTypeInternal[name] = Symbol.named("Roact" .. name)
end

getmetatable(MirroredType).__index = MirroredTypeInternal
getmetatable(MirroredType).__tostring = function()
return "RoactType"
end

strict(MirroredTypeInternal, "Type")

local Mirror = newproxy(true)
local MirrorInternal = {
Type = MirroredType,
typeOf = function(value)
local name = Type.nameOf(Type.of(value))
if not name then
return nil
end
return MirroredTypeInternal[name]
end,
}

getmetatable(Mirror).__index = MirrorInternal
MisterUncloaked marked this conversation as resolved.
Show resolved Hide resolved
getmetatable(Mirror).__tostring = function()
return "TypeMirror"
end

strict(MirrorInternal, "TypeMirror")

return Mirror
MisterUncloaked marked this conversation as resolved.
Show resolved Hide resolved
64 changes: 64 additions & 0 deletions src/TypeMirror.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
return function()
local Type = require(script.Parent.Type)
local Mirror = require(script.Parent.TypeMirror)
local allowedTypes = {
Type.Binding,
Type.Element,
Type.HostChangeEvent,
Type.HostEvent,
Type.StatefulComponentClass,
Type.StatefulComponentInstance,
Type.VirtualTree
}
MisterUncloaked marked this conversation as resolved.
Show resolved Hide resolved

describe("Type", function()
it("should return a mirror of an internal type", function()
local name = Type.nameOf(Type.Element)
local mirroredType = Mirror.Type[name]
expect(mirroredType).to.equal(Mirror.Type.Element)
end)

it("should not return the actual internal type", function()
local name = Type.nameOf(Type.Element)
local mirroredType = Mirror.Type[name]
expect(mirroredType).to.never.equal(Type.Element)
end)

it("should include all allowed types", function()
for _, type in ipairs(allowedTypes) do
local name = Type.nameOf(type)
local mirroredType = Mirror.Type[name]
expect(mirroredType).to.be.ok()
end
end)

it("should not include any other types", function()
local name = Type.nameOf(Type.VirtualNode)
local success = pcall(function()
local _ = Mirror.Type[name]
end)
expect(success).to.equal(false)
end)
end)

describe("typeOf", function()
it("should return nil if the value is not a table", function()
expect(Mirror.typeOf(1)).to.equal(nil)
expect(Mirror.typeOf(true)).to.equal(nil)
expect(Mirror.typeOf("test")).to.equal(nil)
expect(Mirror.typeOf(print)).to.equal(nil)
end)

it("should return nil if the table has no type", function()
expect(Mirror.typeOf({})).to.equal(nil)
end)
MisterUncloaked marked this conversation as resolved.
Show resolved Hide resolved

it("should return the assigned type", function()
local test = {
[Type] = Type.Element
}

expect(Mirror.typeOf(test)).to.equal(Mirror.Type.Element)
end)
end)
end
4 changes: 4 additions & 0 deletions src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ local createReconcilerCompat = require(script.createReconcilerCompat)
local RobloxRenderer = require(script.RobloxRenderer)
local strict = require(script.strict)
local Binding = require(script.Binding)
local TypeMirror = require(script.TypeMirror)

local robloxReconciler = createReconciler(RobloxRenderer)
local reconcilerCompat = createReconcilerCompat(robloxReconciler)
Expand Down Expand Up @@ -37,6 +38,9 @@ local Roact = strict {
teardown = reconcilerCompat.teardown,
reconcile = reconcilerCompat.reconcile,

typeOf = TypeMirror.typeOf,
Type = TypeMirror.Type,

setGlobalConfig = GlobalConfig.set,

-- APIs that may change in the future without warning
Expand Down
2 changes: 2 additions & 0 deletions src/init.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ return function()
update = "function",
oneChild = "function",
setGlobalConfig = "function",
typeOf = "function",

-- These functions are deprecated and throw warnings!
reify = "function",
Expand All @@ -26,6 +27,7 @@ return function()
Event = true,
Change = true,
Ref = true,
Type = true,
None = true,
UNSTABLE = true,
}
Expand Down