hyperapp/index.d.ts

239 lines
8.2 KiB
TypeScript
Raw Permalink Normal View History

// 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>[]
2021-05-05 02:38:07 +02:00
): 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.
2021-05-05 02:38:07 +02:00
function text<T = unknown>(
// Values, aside from symbols and functions, can be handled.
value: T extends symbol | ((..._: unknown[]) => unknown) ? never : T
2021-05-05 02:38:07 +02:00
): 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>[]
2021-05-05 02:38:07 +02:00
): 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]
2022-03-02 13:25:20 +01:00
// An effecter is the function that runs an effect.
type Effecter<S, P = any> = (
dispatch: Dispatch<S>,
payload: P
2022-03-02 13:25:20 +01:00
) => 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]
2021-04-05 11:35:20 +02:00
}
// 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 &
2021-04-18 02:40:33 +02:00
ElementCSSInlineStyle &
GlobalEventHandlers
)> &
ElementCreationOptions &
EventActions<S>
> &
{
2021-04-18 02:40:33 +02:00
[_: string]: unknown
class?: ClassProp
key?: VNode<S>["key"]
2021-04-18 02:40:33 +02:00
style?: StyleProp
// By disallowing `_VNode` we ensure values having the `VNode` type are
// not mistaken for also having the `Props` type.
_VNode?: never
2021-04-18 02:40:33 +02:00
}
>
// 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.
2021-05-05 02:38:07 +02:00
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
2021-05-05 02:38:07 +02:00
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
}
2021-05-05 02:38:07 +02:00
// 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
}