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 application
export 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} variable={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.