From c5ccab0b66ccfe09534c072fc84272e00be3ea62 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Thu, 8 Aug 2019 19:46:03 -0700 Subject: [PATCH 01/16] Addresses https://github.com/Roblox/roact/issues/227 Added a public API for type checking in Roact - Added nameOf function to Type to lookup the string name for any Type - Added TypeMirror module which exposes a subset of types in Type without exposing the internal Roact symbols - Added Roact.Type and Roact.typeOf to expose a type checking API for external use --- src/Type.lua | 14 ++++++++- src/Type.spec.lua | 4 +++ src/TypeMirror.lua | 48 +++++++++++++++++++++++++++++++ src/TypeMirror.spec.lua | 64 +++++++++++++++++++++++++++++++++++++++++ src/init.lua | 4 +++ src/init.spec.lua | 2 ++ 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/TypeMirror.lua create mode 100644 src/TypeMirror.spec.lua diff --git a/src/Type.lua b/src/Type.lua index 156ee0ea..d2ca945a 100644 --- a/src/Type.lua +++ b/src/Type.lua @@ -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") @@ -37,6 +40,14 @@ function TypeInternal.of(value) return value[Type] end +function TypeInternal.nameOf(type) + if typeof(type) ~= "userdata" then + return nil + end + + return TypeNames[type] +end + getmetatable(Type).__index = TypeInternal getmetatable(Type).__tostring = function() @@ -44,5 +55,6 @@ getmetatable(Type).__tostring = function() end strict(TypeInternal, "Type") +strict(TypeNames, "TypeNames") return Type \ No newline at end of file diff --git a/src/Type.spec.lua b/src/Type.spec.lua index f2477093..f5336767 100644 --- a/src/Type.spec.lua +++ b/src/Type.spec.lua @@ -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 \ No newline at end of file diff --git a/src/TypeMirror.lua b/src/TypeMirror.lua new file mode 100644 index 00000000..c2680c82 --- /dev/null +++ b/src/TypeMirror.lua @@ -0,0 +1,48 @@ +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 +getmetatable(Mirror).__tostring = function() + return "TypeMirror" +end + +strict(MirrorInternal, "TypeMirror") + +return Mirror \ No newline at end of file diff --git a/src/TypeMirror.spec.lua b/src/TypeMirror.spec.lua new file mode 100644 index 00000000..d9007054 --- /dev/null +++ b/src/TypeMirror.spec.lua @@ -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 + } + + 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) + + it("should return the assigned type", function() + local test = { + [Type] = Type.Element + } + + expect(Mirror.typeOf(test)).to.equal(Mirror.Type.Element) + end) + end) +end \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index f002f975..09863259 100644 --- a/src/init.lua +++ b/src/init.lua @@ -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) @@ -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 diff --git a/src/init.spec.lua b/src/init.spec.lua index 7fcf79c8..6f3087a0 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -13,6 +13,7 @@ return function() update = "function", oneChild = "function", setGlobalConfig = "function", + typeOf = "function", -- These functions are deprecated and throw warnings! reify = "function", @@ -26,6 +27,7 @@ return function() Event = true, Change = true, Ref = true, + Type = true, None = true, UNSTABLE = true, } From ca0b9e6aebe297a898670dc8002abdaf3ded6463 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 10:37:27 -0700 Subject: [PATCH 02/16] Added comments for TypeMirror.lua and updated docs/changelog with type info --- CHANGELOG.md | 1 + docs/advanced/type-checking.md | 70 ++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + src/TypeMirror.lua | 10 +++++ 4 files changed, 82 insertions(+) create mode 100644 docs/advanced/type-checking.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b343718..a6b1928a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/docs/advanced/type-checking.md b/docs/advanced/type-checking.md new file mode 100644 index 00000000..bc13bfa8 --- /dev/null +++ b/docs/advanced/type-checking.md @@ -0,0 +1,70 @@ +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 a type enumeration and a type checking function. + +## Without Type Checking + +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(titleViewClass, { + -- Props for Title... + }) + }) +end +``` + +Now suppose we want to validate that a titleClass is actually a class using [validateProps](../../api-reference/#validateprops). Unfortunately, the best we can do is inspect 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 titleClass.render then + return true + end + return false, tostring(Header) .. " prop titleClass cannot render" +end +``` + +## With Type Checking + +With type checking, 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 + 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 type = Roact.typeOf(titleClass) + local isElement = type == Roact.Type.Element + local isClass = type == 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 = type == Roact.Type.StatefulComponentClass + return Roact.createElement("Frame", { + -- Props for Frame... + }, { + Title = isElement and title or isClass and Roact.createElement(title, { + -- Props for Title... + }) + }) +end +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 94430efe..e1ec89fe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,7 @@ nav: - Portals: advanced/portals.md - Bindings and Refs: advanced/bindings-and-refs.md - Context: advanced/context.md + - Type Checking: advanced/type-checking.md - Performance Optimization: - Overview: performance/overview.md - Reduce Reconcilation: performance/reduce-reconciliation.md diff --git a/src/TypeMirror.lua b/src/TypeMirror.lua index c2680c82..8647bf64 100644 --- a/src/TypeMirror.lua +++ b/src/TypeMirror.lua @@ -1,3 +1,13 @@ +--[[ + 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 + } +]] + local Type = require(script.Parent.Type) local Symbol = require(script.Parent.Symbol) local strict = require(script.Parent.strict) From 999d6a2681706703d9211c39b63e7b277b49317a Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 12:55:28 -0700 Subject: [PATCH 03/16] Polishing Type checking docs --- ...hecking.md => inspecting-roact-objects.md} | 14 +++--- docs/api-reference.md | 49 +++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 58 insertions(+), 7 deletions(-) rename docs/advanced/{type-checking.md => inspecting-roact-objects.md} (75%) diff --git a/docs/advanced/type-checking.md b/docs/advanced/inspecting-roact-objects.md similarity index 75% rename from docs/advanced/type-checking.md rename to docs/advanced/inspecting-roact-objects.md index bc13bfa8..3bec6151 100644 --- a/docs/advanced/type-checking.md +++ b/docs/advanced/inspecting-roact-objects.md @@ -1,6 +1,8 @@ -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 a type enumeration and a type checking function. +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. -## Without Type Checking +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 Inspection Suppose we want to write a Header component with a prop for the title child element: ```lua @@ -10,19 +12,19 @@ function Header:render() return Roact.createElement("Frame", { -- Props for Frame... }, { - Title = Roact.createElement(titleViewClass, { + Title = Roact.createElement(titleClass, { -- Props for Title... }) }) end ``` -Now suppose we want to validate that a titleClass is actually a class using [validateProps](../../api-reference/#validateprops). Unfortunately, the best we can do is inspect Header to see if it contains characteristics of a Component class: +Now suppose we want to validate that titleClass is actually a class using [validateProps](../../api-reference/#validateprops). Unfortunately, the best we can do is inspect 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 titleClass.render then + if type(titleClass.render) == "function" then return true end return false, tostring(Header) .. " prop titleClass cannot render" @@ -47,7 +49,7 @@ We can even provide props which can be of multiple different Roact object types local Header = Component:extend("Header") Header.validateProps = function() local title = props.title -- Type.Element | Type.StatefulComponentClass - local type = Roact.typeOf(titleClass) + local type = Roact.typeOf(title) local isElement = type == Roact.Type.Element local isClass = type == Roact.Type.StatefulComponentClass if isElement or isClass then diff --git a/docs/api-reference.md b/docs/api-reference.md index a51216ca..6647bd37 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -165,6 +165,13 @@ local function Flex() end ``` +--- + +### Roact.typeOf +
Added in 1.2.0
+ + + --- ### Roact.createRef @@ -356,6 +363,48 @@ See [the Portals guide](../advanced/portals) for a small tutorial and more detai --- +## Enumerations + +### Roact.Type +
Added in 1.2.0
+ +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 diff --git a/mkdocs.yml b/mkdocs.yml index e1ec89fe..daf0d4d8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,7 +23,7 @@ nav: - Portals: advanced/portals.md - Bindings and Refs: advanced/bindings-and-refs.md - Context: advanced/context.md - - Type Checking: advanced/type-checking.md + - Inspecting Roact Objects: advanced/inspecting-roact-objects.md - Performance Optimization: - Overview: performance/overview.md - Reduce Reconcilation: performance/reduce-reconciliation.md From e5ed45822a850d40d9bccc901f095f0368ef10b8 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 12:59:07 -0700 Subject: [PATCH 04/16] Remove Type Checking as a term in docs --- docs/advanced/inspecting-roact-objects.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced/inspecting-roact-objects.md b/docs/advanced/inspecting-roact-objects.md index 3bec6151..48c35ec2 100644 --- a/docs/advanced/inspecting-roact-objects.md +++ b/docs/advanced/inspecting-roact-objects.md @@ -2,7 +2,7 @@ In certain situations, such as when building highly reusable and customizable co 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 Inspection +## Without Object Type Inspection Suppose we want to write a Header component with a prop for the title child element: ```lua @@ -19,7 +19,7 @@ function Header:render() 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 inspect Header to see if it contains characteristics of a Component class: +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() @@ -31,9 +31,9 @@ Header.validateProps = function() end ``` -## With Type Checking +## With Object Type Inspection -With type checking, we can be certain we have a Component class: +With `Roact.typeOf`, we can be certain we have a Component class: ```lua Header.validateProps = function() local titleClass = props.titleClass From 1044fd7fd94a7aa15c5d70314da60fcaa927ca77 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 13:02:26 -0700 Subject: [PATCH 05/16] Lua fixes in Type documentation --- docs/advanced/inspecting-roact-objects.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced/inspecting-roact-objects.md b/docs/advanced/inspecting-roact-objects.md index 48c35ec2..ad9a9fda 100644 --- a/docs/advanced/inspecting-roact-objects.md +++ b/docs/advanced/inspecting-roact-objects.md @@ -49,9 +49,9 @@ We can even provide props which can be of multiple different Roact object types local Header = Component:extend("Header") Header.validateProps = function() local title = props.title -- Type.Element | Type.StatefulComponentClass - local type = Roact.typeOf(title) - local isElement = type == Roact.Type.Element - local isClass = type == Roact.Type.StatefulComponentClass + local titleType = Roact.typeOf(title) + local isElement = titleType == Roact.Type.Element + local isClass = titleType == Roact.Type.StatefulComponentClass if isElement or isClass then return true end @@ -60,7 +60,7 @@ end function Header:render() local title = props.title local isElement = Roact.typeOf(title) == Roact.Type.Element - local isClass = type == Roact.Type.StatefulComponentClass + local isClass = Roact.typeOf(title) == Roact.Type.StatefulComponentClass return Roact.createElement("Frame", { -- Props for Frame... }, { From 3ddc18ba33f0d74cc4b02ddb64006c6ac78ce8be Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 13:03:29 -0700 Subject: [PATCH 06/16] Slightly simpler syntax for Lua code in Type docs --- docs/advanced/inspecting-roact-objects.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/advanced/inspecting-roact-objects.md b/docs/advanced/inspecting-roact-objects.md index ad9a9fda..0742ef3c 100644 --- a/docs/advanced/inspecting-roact-objects.md +++ b/docs/advanced/inspecting-roact-objects.md @@ -49,9 +49,8 @@ We can even provide props which can be of multiple different Roact object types local Header = Component:extend("Header") Header.validateProps = function() local title = props.title -- Type.Element | Type.StatefulComponentClass - local titleType = Roact.typeOf(title) - local isElement = titleType == Roact.Type.Element - local isClass = titleType == Roact.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 From e4962cc8abb0aa283f3b25a80a82614516b30595 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 16:35:44 -0700 Subject: [PATCH 07/16] TypeMirror improvements: - removed userdata dance in TypeMirror.lua - removed redundant checks in TypeMirror.lua - removed redundant declaration of allowed Roact types --- src/Type.lua | 4 ---- src/TypeMirror.lua | 35 ++++++++++++++++++----------------- src/TypeMirror.spec.lua | 30 +++++++++++------------------- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/Type.lua b/src/Type.lua index d2ca945a..18dafefe 100644 --- a/src/Type.lua +++ b/src/Type.lua @@ -41,10 +41,6 @@ function TypeInternal.of(value) end function TypeInternal.nameOf(type) - if typeof(type) ~= "userdata" then - return nil - end - return TypeNames[type] end diff --git a/src/TypeMirror.lua b/src/TypeMirror.lua index 8647bf64..b641989f 100644 --- a/src/TypeMirror.lua +++ b/src/TypeMirror.lua @@ -4,7 +4,7 @@ TypeMirror: { Type: Roact.Type, - typeof: function(value: table) -> Roact.Type | nil + typeOf: function(value: table) -> Roact.Type | nil } ]] @@ -22,37 +22,38 @@ local ALLOWED_TYPES = { Type.VirtualTree } -local MirroredType = newproxy(true) -local MirroredTypeInternal = {} +local MirroredType = {} for _, type in ipairs(ALLOWED_TYPES) do local name = Type.nameOf(type) - MirroredTypeInternal[name] = Symbol.named("Roact" .. name) + MirroredType[name] = Symbol.named("Roact" .. name) end -getmetatable(MirroredType).__index = MirroredTypeInternal -getmetatable(MirroredType).__tostring = function() - return "RoactType" -end +setmetatable(MirroredType, { + __tostring = function() + return "RoactType" + end +}) -strict(MirroredTypeInternal, "Type") +strict(MirroredType, "Type") -local Mirror = newproxy(true) -local MirrorInternal = { +local Mirror = { + typeList = ALLOWED_TYPES, Type = MirroredType, typeOf = function(value) local name = Type.nameOf(Type.of(value)) if not name then return nil end - return MirroredTypeInternal[name] + return MirroredType[name] end, } -getmetatable(Mirror).__index = MirrorInternal -getmetatable(Mirror).__tostring = function() - return "TypeMirror" -end +setmetatable(Mirror, { + __tostring = function() + return "TypeMirror" + end +}) -strict(MirrorInternal, "TypeMirror") +strict(Mirror, "TypeMirror") return Mirror \ No newline at end of file diff --git a/src/TypeMirror.spec.lua b/src/TypeMirror.spec.lua index d9007054..5cd81337 100644 --- a/src/TypeMirror.spec.lua +++ b/src/TypeMirror.spec.lua @@ -1,15 +1,6 @@ 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 - } describe("Type", function() it("should return a mirror of an internal type", function() @@ -25,7 +16,7 @@ return function() end) it("should include all allowed types", function() - for _, type in ipairs(allowedTypes) do + for _, type in ipairs(Mirror.typeList) do local name = Type.nameOf(type) local mirroredType = Mirror.Type[name] expect(mirroredType).to.be.ok() @@ -42,15 +33,16 @@ return function() 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) + it("should throw if the value is not a valid type", function() + local typeOfCheck = function(value) + local _ = Mirror.typeOf(value) + end + expect(pcall(typeOfCheck, 1)).to.equal(false) + expect(pcall(typeOfCheck, true)).to.equal(false) + expect(pcall(typeOfCheck, "test")).to.equal(false) + expect(pcall(typeOfCheck, print)).to.equal(false) + expect(pcall(typeOfCheck, {})).to.equal(false) + expect(pcall(typeOfCheck, newproxy(true))).to.equal(false) end) it("should return the assigned type", function() From 45a39208c27f4baccaae2bb13514ad391120641c Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 17:01:30 -0700 Subject: [PATCH 08/16] Added Roact.isValidElementType to see if a value has the right type to be passed to Roact.createElement --- src/init.lua | 1 + src/init.spec.lua | 1 + src/isValidElementType.lua | 14 ++++++++++++++ src/isValidElementType.spec.lua | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 src/isValidElementType.lua create mode 100644 src/isValidElementType.spec.lua diff --git a/src/init.lua b/src/init.lua index 09863259..a537468b 100644 --- a/src/init.lua +++ b/src/init.lua @@ -38,6 +38,7 @@ local Roact = strict { teardown = reconcilerCompat.teardown, reconcile = reconcilerCompat.reconcile, + isValidElementType = require(script.isValidElementType), typeOf = TypeMirror.typeOf, Type = TypeMirror.Type, diff --git a/src/init.spec.lua b/src/init.spec.lua index 6f3087a0..cb01c723 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -14,6 +14,7 @@ return function() oneChild = "function", setGlobalConfig = "function", typeOf = "function", + isValidElementType = "function", -- These functions are deprecated and throw warnings! reify = "function", diff --git a/src/isValidElementType.lua b/src/isValidElementType.lua new file mode 100644 index 00000000..e87dc94d --- /dev/null +++ b/src/isValidElementType.lua @@ -0,0 +1,14 @@ +local Portal = require(script.Parent.Portal) +local Type = require(script.Parent.Type) + +-- Returns true if the provided object can be used by Roact.createElement +return function(value) + local valueType = type(value) + + local isComponentClass = Type.of(value) == Type.StatefulComponentClass + local isValidFunctionComponentType = valueType == "function" + local isValidHostType = valueType == "string" + local isPortal = value == Portal + + return isComponentClass or isValidFunctionComponentType or isValidHostType or isPortal +end \ No newline at end of file diff --git a/src/isValidElementType.spec.lua b/src/isValidElementType.spec.lua new file mode 100644 index 00000000..6abf6891 --- /dev/null +++ b/src/isValidElementType.spec.lua @@ -0,0 +1,33 @@ +local Component = require(script.Parent.Component) +local Portal = require(script.Parent.Portal) +local createElement = require(script.Parent.createElement) +local isValidElementType = require(script.Parent.isValidElementType) + +return function() + it("should return true for a stateful component class", function() + local MyStatefulComponent = Component:extend("MyStatefulComponent") + expect(isValidElementType(MyStatefulComponent)).to.equal(true) + end) + + it("should return true for a function component", function() + local MyFunctionComponent = function(props) + return createElement("Frame", {}) + end + expect(isValidElementType(MyFunctionComponent)).to.equal(true) + end) + + -- There's no way to guarantee the return type for a function in Lua at the moment + itSKIP("should not return true for a function that returns an invalid type", function() end) + + it("should return true for a string representing a host instance type", function() + local host = "Frame" + expect(isValidElementType(host)).to.equal(true) + end) + + -- In the future, an exhaustive enum of all possible host instance types could enable this check + itSKIP("should not return true for a function that returns an invalid type", function() end) + + it("should return true for a portal", function() + expect(isValidElementType(Portal)).to.equal(true) + end) +end \ No newline at end of file From 0993ed2f9c78d4f864a37b1e2bc8b293495a8b5e Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 18:32:18 -0700 Subject: [PATCH 09/16] Update documentation for type validation --- docs/advanced/inspecting-roact-objects.md | 71 -------------------- docs/advanced/type-validation.md | 79 +++++++++++++++++++++++ docs/api-reference.md | 4 ++ mkdocs.yml | 2 +- 4 files changed, 84 insertions(+), 72 deletions(-) delete mode 100644 docs/advanced/inspecting-roact-objects.md create mode 100644 docs/advanced/type-validation.md diff --git a/docs/advanced/inspecting-roact-objects.md b/docs/advanced/inspecting-roact-objects.md deleted file mode 100644 index 0742ef3c..00000000 --- a/docs/advanced/inspecting-roact-objects.md +++ /dev/null @@ -1,71 +0,0 @@ -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 - 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 -``` \ No newline at end of file diff --git a/docs/advanced/type-validation.md b/docs/advanced/type-validation.md new file mode 100644 index 00000000..4c2630c4 --- /dev/null +++ b/docs/advanced/type-validation.md @@ -0,0 +1,79 @@ +In certain situations, such as when building reusable and customizable components, props may be composed of Roact objects, such as an element or a component. + +To facilitate safer development for these kinds of situations, Roact provides the `Roact.typeOf` and `Roact.isValidElementType` functions to help validate these objects. + +## Without Type Validation + +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() + return Roact.createElement("Frame", { + -- Props for Frame... + }, { + Title = props.title + }) +end +``` + +Now suppose we want to validate that `title` is actually an element using [validateProps](../../api-reference/#validateprops). Without a type checking function, `title` must be queried to check for characteristics of an element: +```lua +Header.validateProps = function() + local title = props.title + + if props.title.component then + return true + end + + return false, tostring(Header) .. " prop title is not an element" +end +``` +This approach is fragile, since it relies on undocumented internals. + +## Roact Object Type Validation + +With `Roact.typeOf` we can be certain we have a Roact Element: +```lua +Header.validateProps = function() + local title = props.title + + if Roact.typeOf(title) == Roact.Type.Element then + return true + end + + return false, tostring(Header) .. " prop title is not an element" +end +``` + +## Element Type Validation + +In some cases, a component will be more preferable as a prop than an element. `Roact.isValidElementType` can be used to see if a value is a plausible component and thus can be passed to `Roact.createElement`. + +```lua +local Header = Component:extend("Header") + +Header.validateProps = function() + local title = props.title + + if Roact.isValidElementType(title) then + return true + end + + return false, tostring(Header) .. " prop title can not be an element" +end + +function Header:render() + local title = props.title + return Roact.createElement("Frame", { + -- Props for Frame... + }, { + Title = Roact.isValidElementType(title) and Roact.createElement(title, { + -- Props for Title... + }) + }) +end +``` + +!!! info + Because strings (hosts) and functions are valid component types, `Roact.isValidElementType` is less safe than `Roact.typeOf`. If safety is paramount, consider only allowing component classes, and checking that the `typeOf` the prop is `Roact.Type.StatefulComponentClass`. \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md index 6647bd37..7868878c 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -170,7 +170,11 @@ end ### Roact.typeOf
Added in 1.2.0
+``` +Roact.typeOf(roactObject) -> Roact.Type +``` +Returns the [Roact.Type](#roacttype) of the passed in Roact object --- diff --git a/mkdocs.yml b/mkdocs.yml index daf0d4d8..09b9d707 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,7 +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 + - Type Validation: advanced/type-validation.md - Performance Optimization: - Overview: performance/overview.md - Reduce Reconcilation: performance/reduce-reconciliation.md From 57be113516f7a361cb96e03141c213931299bdba Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 18:34:31 -0700 Subject: [PATCH 10/16] Add API reference for Roact.isValidElementType --- docs/api-reference.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/api-reference.md b/docs/api-reference.md index 7868878c..1fb18317 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -178,6 +178,17 @@ Returns the [Roact.Type](#roacttype) of the passed in Roact object --- +### Roact.isValidElementType +
Added in 1.2.0
+ +``` +Roact.isValidElementType(value) -> bool +``` + +Returns true is the provided value can be used by [Roact.createElement](#roactcreateelement). + +--- + ### Roact.createRef ``` Roact.createRef() -> Ref From c5be78120e64ccb060bd3a00c8d0ead8fc1ac448 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Fri, 9 Aug 2019 18:35:29 -0700 Subject: [PATCH 11/16] Polish type-validation.md --- docs/advanced/type-validation.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/advanced/type-validation.md b/docs/advanced/type-validation.md index 4c2630c4..afa5fc58 100644 --- a/docs/advanced/type-validation.md +++ b/docs/advanced/type-validation.md @@ -9,10 +9,12 @@ Suppose we want to write a `Header` component with a prop for the title child el local Header = Component:extend("Header") function Header:render() + local title = props.title + return Roact.createElement("Frame", { -- Props for Frame... }, { - Title = props.title + Title = title }) end ``` @@ -22,7 +24,7 @@ Now suppose we want to validate that `title` is actually an element using [valid Header.validateProps = function() local title = props.title - if props.title.component then + if title.component then return true end From 9fc38e89ee4424f0add903d1fb786ab85f048a38 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Mon, 12 Aug 2019 09:33:59 -0700 Subject: [PATCH 12/16] Rename isValidElementType to isComponent --- docs/advanced/type-validation.md | 12 ++++++------ docs/api-reference.md | 4 ++-- src/init.lua | 2 +- src/init.spec.lua | 2 +- src/{isValidElementType.lua => isComponent.lua} | 0 ...alidElementType.spec.lua => isComponent.spec.lua} | 10 +++++----- 6 files changed, 15 insertions(+), 15 deletions(-) rename src/{isValidElementType.lua => isComponent.lua} (100%) rename src/{isValidElementType.spec.lua => isComponent.spec.lua} (77%) diff --git a/docs/advanced/type-validation.md b/docs/advanced/type-validation.md index afa5fc58..430dd838 100644 --- a/docs/advanced/type-validation.md +++ b/docs/advanced/type-validation.md @@ -1,6 +1,6 @@ In certain situations, such as when building reusable and customizable components, props may be composed of Roact objects, such as an element or a component. -To facilitate safer development for these kinds of situations, Roact provides the `Roact.typeOf` and `Roact.isValidElementType` functions to help validate these objects. +To facilitate safer development for these kinds of situations, Roact provides the `Roact.typeOf` and `Roact.isComponent` functions to help validate these objects. ## Without Type Validation @@ -48,9 +48,9 @@ Header.validateProps = function() end ``` -## Element Type Validation +## Component Type Validation -In some cases, a component will be more preferable as a prop than an element. `Roact.isValidElementType` can be used to see if a value is a plausible component and thus can be passed to `Roact.createElement`. +In some cases, a component will be more preferable as a prop than an element. `Roact.isComponent` can be used to see if a value is a plausible component and thus can be passed to `Roact.createElement`. ```lua local Header = Component:extend("Header") @@ -58,7 +58,7 @@ local Header = Component:extend("Header") Header.validateProps = function() local title = props.title - if Roact.isValidElementType(title) then + if Roact.isComponent(title) then return true end @@ -70,7 +70,7 @@ function Header:render() return Roact.createElement("Frame", { -- Props for Frame... }, { - Title = Roact.isValidElementType(title) and Roact.createElement(title, { + Title = Roact.isComponent(title) and Roact.createElement(title, { -- Props for Title... }) }) @@ -78,4 +78,4 @@ end ``` !!! info - Because strings (hosts) and functions are valid component types, `Roact.isValidElementType` is less safe than `Roact.typeOf`. If safety is paramount, consider only allowing component classes, and checking that the `typeOf` the prop is `Roact.Type.StatefulComponentClass`. \ No newline at end of file + Because strings (hosts) and functions are valid component types, `Roact.isComponent` is less safe than `Roact.typeOf`. If safety is paramount, consider only allowing component classes, and checking that the `typeOf` the prop is `Roact.Type.StatefulComponentClass`. \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md index 1fb18317..25ec91ae 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -178,11 +178,11 @@ Returns the [Roact.Type](#roacttype) of the passed in Roact object --- -### Roact.isValidElementType +### Roact.isComponent
Added in 1.2.0
``` -Roact.isValidElementType(value) -> bool +Roact.isComponent(value) -> bool ``` Returns true is the provided value can be used by [Roact.createElement](#roactcreateelement). diff --git a/src/init.lua b/src/init.lua index a537468b..35866824 100644 --- a/src/init.lua +++ b/src/init.lua @@ -38,7 +38,7 @@ local Roact = strict { teardown = reconcilerCompat.teardown, reconcile = reconcilerCompat.reconcile, - isValidElementType = require(script.isValidElementType), + isComponent = require(script.isComponent), typeOf = TypeMirror.typeOf, Type = TypeMirror.Type, diff --git a/src/init.spec.lua b/src/init.spec.lua index cb01c723..c291ac73 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -14,7 +14,7 @@ return function() oneChild = "function", setGlobalConfig = "function", typeOf = "function", - isValidElementType = "function", + isComponent = "function", -- These functions are deprecated and throw warnings! reify = "function", diff --git a/src/isValidElementType.lua b/src/isComponent.lua similarity index 100% rename from src/isValidElementType.lua rename to src/isComponent.lua diff --git a/src/isValidElementType.spec.lua b/src/isComponent.spec.lua similarity index 77% rename from src/isValidElementType.spec.lua rename to src/isComponent.spec.lua index 6abf6891..7fa9d7ce 100644 --- a/src/isValidElementType.spec.lua +++ b/src/isComponent.spec.lua @@ -1,19 +1,19 @@ local Component = require(script.Parent.Component) local Portal = require(script.Parent.Portal) local createElement = require(script.Parent.createElement) -local isValidElementType = require(script.Parent.isValidElementType) +local isComponent = require(script.Parent.isComponent) return function() it("should return true for a stateful component class", function() local MyStatefulComponent = Component:extend("MyStatefulComponent") - expect(isValidElementType(MyStatefulComponent)).to.equal(true) + expect(isComponent(MyStatefulComponent)).to.equal(true) end) it("should return true for a function component", function() local MyFunctionComponent = function(props) return createElement("Frame", {}) end - expect(isValidElementType(MyFunctionComponent)).to.equal(true) + expect(isComponent(MyFunctionComponent)).to.equal(true) end) -- There's no way to guarantee the return type for a function in Lua at the moment @@ -21,13 +21,13 @@ return function() it("should return true for a string representing a host instance type", function() local host = "Frame" - expect(isValidElementType(host)).to.equal(true) + expect(isComponent(host)).to.equal(true) end) -- In the future, an exhaustive enum of all possible host instance types could enable this check itSKIP("should not return true for a function that returns an invalid type", function() end) it("should return true for a portal", function() - expect(isValidElementType(Portal)).to.equal(true) + expect(isComponent(Portal)).to.equal(true) end) end \ No newline at end of file From 0fc8b00b9b1ffa14886030ecb355dec2eeba7e12 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Mon, 12 Aug 2019 09:35:04 -0700 Subject: [PATCH 13/16] Updated changelog to reflect Roact.isComponent --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b1928a..af648228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +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)) +* Added Roact.Type, Roact.typeOf, and Roact.isComponent for Roact object and component 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)) From 4ea1247925913a0c2764ea214d7c8fccf3b86740 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Mon, 12 Aug 2019 15:30:35 -0700 Subject: [PATCH 14/16] Simplify test case of typeOf --- src/TypeMirror.spec.lua | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/TypeMirror.spec.lua b/src/TypeMirror.spec.lua index 5cd81337..10fa1244 100644 --- a/src/TypeMirror.spec.lua +++ b/src/TypeMirror.spec.lua @@ -34,15 +34,12 @@ return function() describe("typeOf", function() it("should throw if the value is not a valid type", function() - local typeOfCheck = function(value) - local _ = Mirror.typeOf(value) - end - expect(pcall(typeOfCheck, 1)).to.equal(false) - expect(pcall(typeOfCheck, true)).to.equal(false) - expect(pcall(typeOfCheck, "test")).to.equal(false) - expect(pcall(typeOfCheck, print)).to.equal(false) - expect(pcall(typeOfCheck, {})).to.equal(false) - expect(pcall(typeOfCheck, newproxy(true))).to.equal(false) + expect(pcall(Mirror.typeOf, 1)).to.equal(false) + expect(pcall(Mirror.typeOf, true)).to.equal(false) + expect(pcall(Mirror.typeOf, "test")).to.equal(false) + expect(pcall(Mirror.typeOf, print)).to.equal(false) + expect(pcall(Mirror.typeOf, {})).to.equal(false) + expect(pcall(Mirror.typeOf, newproxy(true))).to.equal(false) end) it("should return the assigned type", function() From 0d52578cfc0cd0f6c0c6a14242659379f1a53766 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Mon, 12 Aug 2019 15:53:12 -0700 Subject: [PATCH 15/16] Fix name for skipped test in isComponent.spec.lua to reflect its nature --- src/isComponent.spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/isComponent.spec.lua b/src/isComponent.spec.lua index 7fa9d7ce..44a02201 100644 --- a/src/isComponent.spec.lua +++ b/src/isComponent.spec.lua @@ -25,7 +25,7 @@ return function() end) -- In the future, an exhaustive enum of all possible host instance types could enable this check - itSKIP("should not return true for a function that returns an invalid type", function() end) + itSKIP("should not return true for an invalid host component name", function() end) it("should return true for a portal", function() expect(isComponent(Portal)).to.equal(true) From 666d7a98dd0ea5349f2d2c0e72b13e6635af5675 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Mon, 19 Aug 2019 14:21:43 -0700 Subject: [PATCH 16/16] - typeOf no longer throws - removed Without Type Checking example that used undocumented internals - removed info blurb about only accepting component classes for additional safety --- docs/advanced/type-validation.md | 29 +++++------------------------ docs/api-reference.md | 2 +- src/Type.lua | 4 ++++ src/TypeMirror.lua | 6 ------ src/TypeMirror.spec.lua | 14 +++++++------- 5 files changed, 17 insertions(+), 38 deletions(-) diff --git a/docs/advanced/type-validation.md b/docs/advanced/type-validation.md index 430dd838..57856dcb 100644 --- a/docs/advanced/type-validation.md +++ b/docs/advanced/type-validation.md @@ -2,7 +2,7 @@ In certain situations, such as when building reusable and customizable component To facilitate safer development for these kinds of situations, Roact provides the `Roact.typeOf` and `Roact.isComponent` functions to help validate these objects. -## Without Type Validation +## Roact Object Type Validation Suppose we want to write a `Header` component with a prop for the title child element: ```lua @@ -19,23 +19,7 @@ function Header:render() end ``` -Now suppose we want to validate that `title` is actually an element using [validateProps](../../api-reference/#validateprops). Without a type checking function, `title` must be queried to check for characteristics of an element: -```lua -Header.validateProps = function() - local title = props.title - - if title.component then - return true - end - - return false, tostring(Header) .. " prop title is not an element" -end -``` -This approach is fragile, since it relies on undocumented internals. - -## Roact Object Type Validation - -With `Roact.typeOf` we can be certain we have a Roact Element: +Now suppose we want to validate that `title` is actually an element using [validateProps](../../api-reference/#validateprops). With `Roact.typeOf` we can be certain we have a Roact Element: ```lua Header.validateProps = function() local title = props.title @@ -44,7 +28,7 @@ Header.validateProps = function() return true end - return false, tostring(Header) .. " prop title is not an element" + return false, "prop title is not an element" end ``` @@ -62,7 +46,7 @@ Header.validateProps = function() return true end - return false, tostring(Header) .. " prop title can not be an element" + return false, "prop title can not be an element" end function Header:render() @@ -75,7 +59,4 @@ function Header:render() }) }) end -``` - -!!! info - Because strings (hosts) and functions are valid component types, `Roact.isComponent` is less safe than `Roact.typeOf`. If safety is paramount, consider only allowing component classes, and checking that the `typeOf` the prop is `Roact.Type.StatefulComponentClass`. \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md index 25ec91ae..7465d380 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -174,7 +174,7 @@ end Roact.typeOf(roactObject) -> Roact.Type ``` -Returns the [Roact.Type](#roacttype) of the passed in Roact object +Returns the [Roact.Type](#roacttype) of the passed in Roact object, or `nil` if the input is not a Roact object. --- diff --git a/src/Type.lua b/src/Type.lua index 18dafefe..d2ca945a 100644 --- a/src/Type.lua +++ b/src/Type.lua @@ -41,6 +41,10 @@ function TypeInternal.of(value) end function TypeInternal.nameOf(type) + if typeof(type) ~= "userdata" then + return nil + end + return TypeNames[type] end diff --git a/src/TypeMirror.lua b/src/TypeMirror.lua index b641989f..091118ed 100644 --- a/src/TypeMirror.lua +++ b/src/TypeMirror.lua @@ -48,12 +48,6 @@ local Mirror = { end, } -setmetatable(Mirror, { - __tostring = function() - return "TypeMirror" - end -}) - strict(Mirror, "TypeMirror") return Mirror \ No newline at end of file diff --git a/src/TypeMirror.spec.lua b/src/TypeMirror.spec.lua index 10fa1244..46448777 100644 --- a/src/TypeMirror.spec.lua +++ b/src/TypeMirror.spec.lua @@ -33,13 +33,13 @@ return function() end) describe("typeOf", function() - it("should throw if the value is not a valid type", function() - expect(pcall(Mirror.typeOf, 1)).to.equal(false) - expect(pcall(Mirror.typeOf, true)).to.equal(false) - expect(pcall(Mirror.typeOf, "test")).to.equal(false) - expect(pcall(Mirror.typeOf, print)).to.equal(false) - expect(pcall(Mirror.typeOf, {})).to.equal(false) - expect(pcall(Mirror.typeOf, newproxy(true))).to.equal(false) + it("should return nil if the value is not a valid type", 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) + expect(Mirror.typeOf({})).to.equal(nil) + expect(Mirror.typeOf(newproxy(true))).to.equal(nil) end) it("should return the assigned type", function()