Typed State
Describing the possible message identifiers and their types allows Electric UI to strictly type data handling pipelines, user interface usage, and provide warnings if new messages are sent from hardware.
The typedState file?
While technically optional, we do recommend implementing and using strict types across the user interface for a more sane development experience.
The template provides a src/application/typedState.ts
file by default which follows this structure:
declare global { interface ElectricUIDeveloperState { [messageID : string]: any }} // Export custom struct types for use in both codecs and the applicationexport type ExampleType = { sensor_a : number sensor_b : number} // This exports these types into the dependency tree.export {}
This file is responsible for two things:
- Global definition of the types used with the developer-scoped message identifiers
- Providing a centralised location for custom type definitions
Default Types
This allows us to tell the UI which message ID's we're expecting to see from hardware, and the type of those messages.
declare global { interface ElectricUIDeveloperState { [messageID : string]: any // Example typings for simple single value packets led_blink : number led_state : number lit_time : number }}
The first entry provides a default type of any
to all message identifiers, allowing the developer to add stricter definitions as the project matures.
The following entries override the any
type for the led_blink
, led_state
and lit_time
variables used in the hello-blink tutorial. In this example, they are individual number
values.
Custom Types
Custom types should be defined and exported from typedState.ts
. These are normal Typescript types, and can also include enum values, templated types, intersection types (unions) and so on.
export enum LED_STATES { LED_OFF = 0, LED_ON ,} export type LEDSettings = { glow_time : number enable : LED_STATES } export type BuildInfo = { branch : string author : string git_tag : string version : number}
The most common usage of these custom types is to strictly type codecs as described in the custom types guide.
Using custom types in the UI
Import the specific type you need in your file, using the relative path to the typedState
file:
import { LEDSettings } from '../../application/typedState'
Centralise msgID strings
The typedState file provides a central location to define the list of messageID's as an enum. If you prefer, you can create and use a different file to keep the MSGID's separate from other types.
Unlike C, Typescript enum's aren't limited to integer types.
export enum MSGID { NICKNAME = 'name', FAN_SPEED = 'fanspd', SUPERVISOR = 'sup',}
To use these values in UI layouts, just import the MSGID enum from the typedState file:
import { MSGID } from 'src/application/typedState'
Where some components can be passed a string directly, the { }
syntax is needed because Typescript needs to reduce enum member MSGID.FAN_SPEED
down to the string fanspd
.
<IntervalRequester interval ={100} messageID ={MSGID .FAN_SPEED } />
When using accessor syntax with structured objects, the same basic usage applies:
const supervisor_state = useHardwareState ( state => state [MSGID .SUPERVISOR ].control_state )
Enforcing types with accessors and writers
Because strictly typing message identifiers and enforcing typedState usage can be a poor user experience during rapid prototyping, we don't enforce it by default.
In typedState.ts
find and remove the line:
declare global { interface ElectricUIDeveloperState { [messageID : string]: any }}
For messageID's which use a custom type, use the custom type you've defined and use with your codec.
declare global { interface ElectricUIDeveloperState { blink_mode : number lit_time : number fw_info : BuildInfo }}
At this point, all message identifiers need to be defined. This includes ID's using default types, their messageID will typically be number
or string
.
Failure to define the message ID and type will cause type errors when attempting to run the user interface.
Undefined Message ID Guard
During prototyping, it's common to add temporary message identifiers to visualise a signal or provide debug interaction. Electric UI provides a guard feature which throws errors and warnings when interacting with message identifiers which aren't defined completely.
In src/transport-manager/config/serial.tsx
, a component called UndefinedMessageIDGuardPipeline
is created and provided with a typeCache
. It also accepts user defined message identifiers including our default name
packet.
This functionality is then added as one of the pipelines which make up a given transport.
const cobsPipeline = new COBSPipeline() const binaryPipeline = new BinaryPipeline() const typeCachePipeline = new BinaryTypeCachePipeline(typeCache) // If you have runtime generated messageIDs, add them as an array as a second argument // `name` is added because it is requested by the metadata requester before handshake. const undefinedMessageIDGuard = new UndefinedMessageIDGuardPipeline( typeCache, ['name'], ) const codecPipeline = new CodecDuplexPipelineWithDefaults() const largePacketPipeline = new BinaryLargePacketHandlerPipeline({ connectionInterface , maxPayloadLength: 100, }) connectionInterface .setPipelines([ cobsPipeline, binaryPipeline, largePacketPipeline , codecPipeline, typeCachePipeline, undefinedMessageIDGuard, ]) return connectionInterface.finalise() },)
This component can be commented out, or added, at any point in the project's life-cycle.