diff --git a/Pract/Types.lua b/Pract/Types.lua index 92c4e97..e9f6fed 100644 --- a/Pract/Types.lua +++ b/Pract/Types.lua @@ -4,10 +4,10 @@ -- Util types export type ChildrenArgument = {[any]: Element | boolean | nil} -export type PropsArgument = {[any]: any} +export type PropsArgument = any -- {[any]: any} export type Symbol = {} ---export type StateUpdate = {[any]: any} | (state: S, props: P) -> {[any]: any} ---export type SetStateCB = (stateUpdate: StateUpdate) -> () +--export type StateUpdate = {[any]: any} | (state: P, props: P) -> {[any]: any} +--export type SetStateCB = (stateUpdate: StateUpdate) -> () @@ -18,9 +18,12 @@ export type Element = { export type Component = (props: any) -> Element export type ComponentTyped = (props: PropsType) -> Element --export type ComponentTyped

= (props: P) -> any -export type ClassState = {[string]: any} -export type ClassStateUpdateThunk = (state: ClassState, props: PropsArgument) -> ClassState -export type ClassStateUpdate = ClassState | ClassStateUpdateThunk +export type ClassState = any +export type PartialClassState = {[string]: any} +export type ClassStateUpdateThunk = (state: ClassState, props: PropsArgument) -> PartialClassState +export type ClassStateUpdateThunkTyped = (state: P, props: P) -> S +export type ClassStateUpdate = PartialClassState | ClassStateUpdateThunk +export type ClassStateUpdateTyped = S | ClassStateUpdateThunkTyped export type ClassComponentSelf = { [any]: any, props: PropsArgument, @@ -32,6 +35,17 @@ export type ClassComponentSelf = { subscribeState: (self: ClassComponentSelf, listener: () -> ()) -> (() -> ()), forceUpdate: (self: ClassComponentSelf) -> (), } +export type ClassComponentSelfTyped = { + [any]: (self: ClassComponentSelfTyped, ...any) -> ...any, + props: P, + state: S, + setState: ( + self: ClassComponentSelfTyped, + partialStateUpdate: ClassStateUpdateTyped + ) -> (), + subscribeState: (self: ClassComponentSelfTyped, listener: () -> ()) -> (() -> ()), + forceUpdate: (self: ClassComponentSelfTyped) -> (), +} export type ClassComponentMethods = { [any]: any, render: (self: ClassComponentSelf) -> Element, @@ -46,6 +60,20 @@ export type ClassComponentMethods = { didUpdate: ((self: ClassComponentSelf) -> ())?, willUnmount: ((self: ClassComponentSelf) -> ())?, } +export type ClassComponentMethodsTyped = { + [any]: any, + render: (self: ClassComponentSelfTyped) -> Element, + init: ((self: ClassComponentSelfTyped) -> ())?, + didMount: ((self: ClassComponentSelfTyped) ->())?, + shouldUpdate: (( + self: ClassComponentSelfTyped, + newProps: P, + newState: S + ) -> boolean)?, + willUpdate: ((self: ClassComponentSelfTyped, newProps: P, newState: S) -> ())?, + didUpdate: ((self: ClassComponentSelfTyped) -> ())?, + willUnmount: ((self: ClassComponentSelfTyped) -> ())?, +} export type Lifecycle = { render: Component, init: ((props: any) -> ())?, @@ -55,6 +83,15 @@ export type Lifecycle = { didUpdate: ((props: any) -> ())?, willUnmount: ((props: any) -> ())?, } +export type LifecycleTyped

= { + render: Component, + init: ((props: P) -> ())?, + didMount: ((props: P) ->())?, + shouldUpdate: ((newProps: P, oldProps: P) -> boolean)?, + willUpdate: ((props: P, oldProps: P) -> ())?, + didUpdate: ((props: P) -> ())?, + willUnmount: ((props: P) -> ())?, +} diff --git a/Pract/init.lua b/Pract/init.lua index 62d8ae2..dc7c901 100644 --- a/Pract/init.lua +++ b/Pract/init.lua @@ -5,7 +5,7 @@ -- https://ambers-careware.github.io/pract/ local Pract = {} -Pract._VERSION = '0.9.7' +Pract._VERSION = '0.9.8' local Types = require(script.Types) local PractGlobalSystems = require(script.PractGlobalSystems) @@ -19,8 +19,12 @@ export type Element = Types.Element export type PropsArgument = Types.PropsArgument export type ChildrenArgument = Types.ChildrenArgument export type ClassComponentMethods = Types.ClassComponentMethods +-- export type ClassComponentMethodsTyped = Types.ClassComponentMethodsTyped +export type ClassComponentSelf = Types.ClassComponentSelf +-- export type ClassComponentSelfTyped = Types.ClassComponentSelfTyped export type ClassState = Types.ClassState export type Lifecycle = Types.Lifecycle +-- export type LifecycleTyped

= Types.LifecycleTyped

-- Public library values @@ -46,12 +50,18 @@ Pract.unmount = robloxReconciler.unmountVirtualTree -- Higher-order component wrapper functions Pract.withLifecycle = require(script.withLifecycle) +-- Pract.withLifecycleTyped = (Pract.withLifecycle :: any) ::

( +-- closureCreator: (forceUpdate: () -> ()) -> LifecycleTyped

+-- ) -> ComponentTyped

Pract.withState = require(script.withState) Pract.withDeferredState = require(script.withDeferredState) Pract.withSignal = require(script.withSignal) Pract.withContextProvider = require(script.withContextProvider) Pract.withContextConsumer = require(script.withContextConsumer) Pract.classComponent = require(script.classComponent) +-- Pract.classComponentTyped = (Pract.classComponent :: any) :: ( +-- methods: ClassComponentMethodsTyped +-- ) -> ComponentTyped

-- Symbols: diff --git a/docs/advanced/externalstate.md b/docs/advanced/externalstate.md index 2399f12..40a2281 100644 --- a/docs/advanced/externalstate.md +++ b/docs/advanced/externalstate.md @@ -243,7 +243,4 @@ As long as your custom state system has a way to detect changes in state, you ca Try to avoid repeating yourself, and make your own helper higher-order functions or components to connect your third-party state with Pract. That way, using external state can be just as easy as using a pre-made utlilty with your Pract component! -#### Up Next: ??? - -You've reached the end of the Pract documentation. -Much of this documentation is a first draft, and may be subject to change in the future. Collaborators would be appreciated in improving the documentation and functionality of the Pract library in general! \ No newline at end of file +#### Up Next: [Type Safety](./typesafety) \ No newline at end of file diff --git a/docs/advanced/typesafety.md b/docs/advanced/typesafety.md new file mode 100644 index 0000000..d85ae03 --- /dev/null +++ b/docs/advanced/typesafety.md @@ -0,0 +1,243 @@ +--- +layout: default +title: Type Safety +nav_order: 7 +parent: Advanced Guide +permalink: advanced/typesafety +--- + +# Type Safety + +> Note: This section assumes you have a basic understanding of Luau's [type system](https://luau-lang.org/typecheck). + +Pract is compatible with Luau's [type system](https://luau-lang.org/typecheck), and provides variants of previously-discussed features for creating typesafe components! + +Pract's design puts an emphasis on type system use being _opt-in_, as some people may not wish to use Luau's type system in their code. Pract's default constructs have lenient typings, while offering constructs with stricter typings. + +At the time this page is being written, Luau's type system is somewhat underdeveloped, and as such, using Pract with types in strict mode requires a number of conventional tricks to properly assert or annotate types. This article is not completely future-proof, and Pract's type constructs are subject to change. For now, using types with Pract is possible as long as you remember to annotate types where needed. + +## Typing component props + +Pract exports a type `ComponentTyped` that allows you to annotate your components as having a particular type. +A good rule of thumb when typing components is to export the component's props with the module containing the component's code. + +Example: +```lua +--!strict + +local Pract = require(game.ReplicatedStorage.Pract) + +-- This type can be checked statically both in our component function, and with external code using +-- our component. +export type Props = { + size: UDim2, +} +-- Here, we annotate our component to be typed with our props +local MyComponent: Pract.ComponentTyped = function(props) + return Pract.create("Frame", { + Size = props.size, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + }) +end + +return MyComponent +``` + +## Using a typed component externally + +In order for our `Props` type to be validated by exeternal code, we need to use the [`Pract.createTyped`](../api/elements#pract_createtyped) function, which will validate the props we provide to our component. + +```lua +--!strict + +local Pract = require(game.ReplicatedStorage.Pract) +local MyComponent = require(game.ReplicatedStorage.MyComponent) + +local function App(props: {}) + -- If this props table being passed through does not match the MyComponent.Props type, we will + -- see script analysis warnings about the type mismatch! + return Pract.createTyped(MyComponent, { + size = UDim2.fromOffset(200, 300), + }) +end + +return App +``` + +## Typing components using Higher-Order Functions + +One pitfall/necessary convention for using types under roblox's current type system is that, if you use Pract's higher-order functions, you will need to place type annotations in the correct place in order to have proper type safety. + +Consider the following example of a lifecycle component: + +```lua +--!strict + +-- THIS EXAMPLE WILL ERROR AT RUNTIME! + +local Pract = require(game.ReplicatedStorage.Pract) + +export type Props = { + size: UDim2, +} +local MyComponent: Pract.ComponentTyped = Pract.withLifecycle(function(forceUpdate) + return { + render = function(props) -- props will be typed as any by default! + return Pract.create("Frame", { + Size = props.THIS_SHOULD_NOT_EXIST, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + }) + end + } +end) + +return MyComponent +``` + +Even though our `Props` type will be checked in code using this component, the component itself does not correctly type `props`, and under Roblox's current type system, there is no way to create good constructs that will automate this. For now, make sure you manually give `props` a type annotation: + +```lua +--!strict + +-- THIS EXAMPLE WILL ERROR AT RUNTIME! +-- However, we will be able to catch this error through type checking before even running the game! + +local Pract = require(game.ReplicatedStorage.Pract) + +export type Props = { + size: UDim2, +} +local MyComponent: Pract.ComponentTyped = Pract.withLifecycle(function(forceUpdate) + return { + render = function(props: Props) -- Here, we manually annotate our props type + return Pract.create("Frame", { + Size = props.THIS_SHOULD_NOT_EXIST, -- We will correctly see a script analysis warning here! + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + }) + end + } +end) + +return MyComponent +``` + +## Typing class components + +Class components probably require the largest amount of manual effort in typing, and can never be 100% typesafe. However, there are still some conventions that can be used to improve type safety while writing class components: + +```lua +--!strict + +local Pract = require(game.ReplicatedStorage.Pract) + +type State = { -- We generally don't need to export our state type, since it is private. + hovering: boolean, +} +export type Props = { + size: UDim2, +} +local MyClassComponent: Pract.ComponentTyped = Pract.classComponent({ + init = function(self) + -- The type of "self" will automatically be inferred to a generic Pract + -- type ("Pract.ClassComponentSelf"); however, state and props will by typed as "any" by + -- default. As such, we need to annotate our types strategically. + + -- Here, it is a good idea to store our initial state in a typed variable: + local initialState: State = { + hovering = false, + } + self:setState(initialState) + end, + render = function(self) + -- Because props and state are typed as any, it is a good idea to store them into annotated + -- variables here: + local state: State = self.state + local props: Props = self.props + + return Pract.create("Frame", { + Size = props.size, -- Here, "props.size" is type-checked! + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundColor3 = if state.hovering -- Here, "state.hovering" is type-checked! + then Color3.fromRGB(0, 120, 255) + else Color3.fromRGB(255, 255, 255), + MouseEnter = function() + self:setState({ + hovering = true -- This currently can't be type-checked, unfortunately. + }) + end, + MouseLeave = function() + self:setState({ + hovering = false + }) + end, + }) + end, +}) + +return MyClassComponent +``` + +## Custom class component methods + +Currently, class components are typed with a `{[any]: any}` index type. This means you can add custom methods to your class, but they will by typed as `any`. This means that `self` will not be automatically typed unlike other lifecycle methods. One pitfall of this is that they currently cannot be typechecked, so be wary of this: + +```lua +--!strict + +local Pract = require(game.ReplicatedStorage.Pract) + +type State = { + hovering: boolean, +} +export type Props = { + size: UDim2, +} +local MyClassComponent: Pract.ComponentTyped = Pract.classComponent({ + init = function(self) + local initialState: State = { + hovering = false, + } + self:setState(initialState) + end, + -- Note: we should manually type "self" in custom methods! + setHovering = function(self: Pract.ClassComponentSelf, hovering: boolean) + self:setState({ + hovering = hovering, + }) + end, + render = function(self) + local state: State = self.state + local props: Props = self.props + + return Pract.create("Frame", { + Size = props.size, + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundColor3 = if state.hovering + then Color3.fromRGB(0, 120, 255) + else Color3.fromRGB(255, 255, 255), + MouseEnter = function() + self:setHovering(true) -- This method will not be type-checked! + end, + MouseLeave = function() + self:setHovering(false) + end, + }) + end, +}) + +return MyClassComponent +``` + +## Conclusion + +Using types with Pract is useful for scaled codebases, and can make it so that you never pass in bad props to a component. However, this requires some manual type annotation, and may not be suitable for every project. Consider whether and/or where type safety is necessary in your project. The conventions of type annotations in Pract code may be harder to understand to people unversed in Pract/Luau's type system in general. On the other hand, type constructs can catch errors in your code before even running a playtest. + +#### Up Next: ??? + +You've reached the end of the Pract documentation. +Much of this documentation is a first draft, and may be subject to change in the future. Collaborators would be appreciated in improving the documentation and functionality of the Pract library in general! \ No newline at end of file diff --git a/docs/api/elements.md b/docs/api/elements.md index abc4243..e267fff 100644 --- a/docs/api/elements.md +++ b/docs/api/elements.md @@ -97,6 +97,7 @@ Returns an element which instructs pract to mount multiple elements with the sam ## Pract.createTyped +See: [Type Safety](../advanced/typesafety) for more details on using Pract's optional type features. ```lua Pract.createTyped(