239 lines
8.2 KiB
TypeScript
239 lines
8.2 KiB
TypeScript
// Minimum TypeScript Version: 4.2
|
||
|
||
// This requires every property of an object or none at all.
|
||
type AllOrNothing<T> = T | { [K in keyof T]?: never }
|
||
|
||
// This ensures at least one property in an object is present.
|
||
type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]
|
||
// Credit: https://stackoverflow.com/a/59987826/1935675
|
||
|
||
// This ensures at least one object property group is present.
|
||
type AtLeastSomething<T, U> = U | AtLeastOne<T> & AllOrNothing<U>
|
||
|
||
// Most event typings are provided by TypeScript itself.
|
||
type EventsMap =
|
||
& { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
|
||
& { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
|
||
& { onsearch: Event }
|
||
|
||
// Indexable values are able to use subscripting.
|
||
type Indexable = string | unknown[] | Record<string, any>
|
||
|
||
// This validates plain objects while invalidating array objects and string
|
||
// objects by disallowing numerical indexing.
|
||
type IndexableByKey = Record<number, never>
|
||
|
||
// Empty strings can cause issues in certain places.
|
||
type NonEmptyString<T> = T extends "" ? never : T
|
||
|
||
// -----------------------------------------------------------------------------
|
||
|
||
declare module "hyperapp" {
|
||
// `app()` initiates a Hyperapp instance. Only `app()`'s `node:` property and
|
||
// effecters and subscribers are allowed to have side effects.
|
||
function app<S>(props: App<S>): Dispatch<S>
|
||
|
||
// `h()` builds a virtual DOM node.
|
||
function h<S, C = unknown, T extends string = string>(
|
||
tag: NonEmptyString<T>,
|
||
props: CustomPayloads<S, C> & Props<S>,
|
||
children?: MaybeVNode<S> | readonly MaybeVNode<S>[]
|
||
): ElementVNode<S>
|
||
|
||
// `memo()` stores a view along with any given data for it.
|
||
function memo<S, D extends Indexable = Indexable>(
|
||
view: (data: D) => VNode<S>,
|
||
data: D
|
||
): VNode<S>
|
||
|
||
// `text()` creates a virtual DOM node representing plain text.
|
||
function text<T = unknown>(
|
||
// Values, aside from symbols and functions, can be handled.
|
||
value: T extends symbol | ((..._: unknown[]) => unknown) ? never : T
|
||
): TextVNode
|
||
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// This lets you make a variant of `h()` which is aware of your Hyperapp
|
||
// instance's state. The `_ extends never` ensures that any state-aware
|
||
// `h()` doesn't have an explicit state type that contradicts the
|
||
// state type it actually uses.
|
||
interface TypedH<S> {
|
||
<_ extends never, C = unknown, T extends string = string>(
|
||
tag: NonEmptyString<T>,
|
||
props: CustomPayloads<S, C> & Props<S>,
|
||
children?: MaybeVNode<S> | readonly MaybeVNode<S>[]
|
||
): ElementVNode<S>
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// An action transforms existing state and/or wraps another action.
|
||
type Action<S, P = any> = (state: S, payload: P) => Dispatchable<S>
|
||
|
||
// A Hyperapp instance typically has an initial state and a top-level view
|
||
// mounted over an available DOM element.
|
||
type App<S> =
|
||
Readonly<AtLeastSomething<{
|
||
// State is established through either direct assignment or an action.
|
||
init: Dispatchable<S>
|
||
|
||
// The subscriptions function manages a set of subscriptions.
|
||
subscriptions: (state: S) =>
|
||
readonly (boolean | undefined | Subscription<S>)[]
|
||
|
||
// Dispatching can be augmented to do custom processing.
|
||
dispatch: (dispatch: Dispatch<S>) => Dispatch<S>
|
||
}, {
|
||
// The top-level view can build a virtual DOM node depending on the state.
|
||
view: (state: S) => VNode<S>
|
||
|
||
// The mount node is where a Hyperapp instance will get placed.
|
||
node: Node
|
||
}>>
|
||
|
||
// The `class` property represents an HTML class attribute string.
|
||
type ClassProp =
|
||
| boolean
|
||
| string
|
||
| undefined
|
||
| Record<string, boolean | undefined>
|
||
| ClassProp[]
|
||
|
||
// This lets event-handling actions properly accept custom payloads.
|
||
type CustomPayloads<S, T> = {
|
||
[K in keyof T]?:
|
||
K extends "style"
|
||
? StyleProp
|
||
: T[K] extends [action: Action<S, infer P>, payload: unknown]
|
||
? readonly [action: Action<S, P>, payload: P]
|
||
: T[K]
|
||
}
|
||
|
||
// Dispatching will cause state transitions.
|
||
type Dispatch<S> = (dispatchable: Dispatchable<S>, payload?: unknown) => void
|
||
|
||
// A dispatchable entity is used to cause a state transition.
|
||
type Dispatchable<S, P = any> =
|
||
| S
|
||
| [state: S, ...effects: MaybeEffect<S, P>[]]
|
||
| Action<S, P>
|
||
| readonly [action: Action<S, P>, payload: P]
|
||
|
||
// An effecter is the function that runs an effect.
|
||
type Effecter<S, P = any> = (
|
||
dispatch: Dispatch<S>,
|
||
payload: P
|
||
) => void | Promise<void>
|
||
|
||
// An effect is where side effects and any additional dispatching may occur.
|
||
type Effect<S, P = any> =
|
||
| Effecter<S, P>
|
||
| readonly [effecter: Effecter<S, P>, payload: P]
|
||
|
||
|
||
// Effects can be declared conditionally.
|
||
type MaybeEffect<S, P> = null | undefined | boolean | "" | 0 | Effect<S, P>
|
||
|
||
|
||
// Event handlers are implemented using actions.
|
||
type EventActions<S> = {
|
||
[K in keyof EventsMap]:
|
||
| Action<S, EventsMap[K]>
|
||
| readonly [action: Action<S>, payload: unknown]
|
||
}
|
||
|
||
// In certain places a virtual DOM node can be made optional.
|
||
type MaybeVNode<S> = boolean | null | undefined | VNode<S>
|
||
|
||
// Virtual DOM properties will often correspond to HTML attributes.
|
||
type Props<S> =
|
||
Readonly<
|
||
Partial<
|
||
Omit<HTMLElement, keyof (
|
||
DocumentAndElementEventHandlers &
|
||
ElementCSSInlineStyle &
|
||
GlobalEventHandlers
|
||
)> &
|
||
ElementCreationOptions &
|
||
EventActions<S>
|
||
> &
|
||
{
|
||
[_: string]: unknown
|
||
class?: ClassProp
|
||
key?: VNode<S>["key"]
|
||
style?: StyleProp
|
||
|
||
// By disallowing `_VNode` we ensure values having the `VNode` type are
|
||
// not mistaken for also having the `Props` type.
|
||
_VNode?: never
|
||
}
|
||
>
|
||
|
||
// The `style` property represents inline CSS. This relies on TypeScript's CSS
|
||
// property definitions. Custom properties aren't covered as well as any newer
|
||
// properties yet to be recognized by TypeScript. The only way to accommodate
|
||
// them is to relax the adherence to TypeScript's CSS property definitions.
|
||
// It's a poor trade-off given the likelihood of using such properties.
|
||
// However, you can use type casting if you want to use them.
|
||
type StyleProp = IndexableByKey & {
|
||
[K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
|
||
}
|
||
|
||
// A subscription reacts to external activity.
|
||
type Subscription<S, P = any> = readonly [
|
||
subscriber: (dispatch: Dispatch<S>, payload: P) => Unsubscribe,
|
||
payload: P
|
||
]
|
||
|
||
// An unsubscribe function cleans up a canceled subscription.
|
||
type Unsubscribe = () => void
|
||
|
||
// A virtual DOM node (a.k.a. VNode) represents an actual DOM element.
|
||
type ElementVNode<S> = {
|
||
readonly props: Props<S>
|
||
readonly children: readonly MaybeVNode<S>[]
|
||
node: null | undefined | Node
|
||
|
||
// Hyperapp takes care of using native Web platform event handlers for us.
|
||
events?:
|
||
Record<
|
||
string,
|
||
Action<S> | readonly [action: Action<S>, payload: unknown]
|
||
>
|
||
|
||
// A key can uniquely associate a VNode with a certain DOM element.
|
||
readonly key: string | null | undefined
|
||
|
||
// A VNode's tag is either an element name or a memoized view function.
|
||
readonly tag: string | ((data: Indexable) => VNode<S>)
|
||
|
||
// If the VNode's tag is a function then this data will get passed to it.
|
||
memo?: Indexable
|
||
|
||
// VNode types are based on actual DOM node types:
|
||
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
|
||
readonly type: 1
|
||
|
||
// `_VNode` is a phantom guard property which gives us a way to tell `VNode`
|
||
// objects apart from `Props` objects. Since we don't expect users to make
|
||
// their own VNodes manually, we can take advantage of this trick which
|
||
// is unique to TypeScript type definitions for JavaScript code.
|
||
_VNode: true
|
||
}
|
||
|
||
// Certain VNodes specifically represent Text nodes and don't rely on state.
|
||
type TextVNode = {
|
||
readonly props: {}
|
||
readonly children: []
|
||
node: null | undefined | Node
|
||
readonly key: undefined
|
||
readonly tag: string
|
||
readonly type: 3
|
||
_VNode: true
|
||
}
|
||
|
||
// VNodes may represent either Text or Element nodes.
|
||
type VNode<S> = ElementVNode<S> | TextVNode
|
||
}
|