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:

tsx
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.

tsx
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.

tsx
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:

tsx
import { LEDSettings } from '../../application/typedState'

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:

tsx
[messageID: string]: any

For messageID's which use a custom type, use the custom type you've defined and use with your codec.

tsx
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.

tsx
const serialTransportFactory = new TransportFactory(
// ...
const undefinedMessageIDGuard = new UndefinedMessageIDGuardPipeline(
typeCache,
['name'],
)
// ...
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.