From 00880bec5572dcdfa34a999068af0114cdefa4a8 Mon Sep 17 00:00:00 2001 From: littensy <56808540+littensy@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:32:23 -0800 Subject: [PATCH] feat: export getter and setter types --- packages/charm/src/index.d.ts | 43 ++++++++++++++++------------------- packages/charm/src/init.luau | 40 ++++++++++++++++---------------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/packages/charm/src/index.d.ts b/packages/charm/src/index.d.ts index 554c327..ebbacc9 100644 --- a/packages/charm/src/index.d.ts +++ b/packages/charm/src/index.d.ts @@ -1,4 +1,5 @@ type Key = string | number | symbol; + type Cleanup = () => void; export enum ReactiveFlags { @@ -31,11 +32,15 @@ export interface Link { } export interface Atom { - (newValue: T | ((currentValue: T) => T)): T; + (update: Update): T; (): T; } -export type Setter = (newValue: T | ((currentValue: T) => T)) => T; +export type Getter = () => T; + +export type Setter = (update: Update) => T; + +export type Update = ((currentValue: T) => T) | T; export type Equals = (current: T, incoming: T) => boolean; @@ -71,16 +76,8 @@ export const flags: { * @returns A tuple containing the getter and setter functions for the signal. * @see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals */ -export function signal( - initialValue: T, - equals?: Equals, -): LuaTuple<[getter: () => T, setter: (newValue: T | ((currentValue: T) => T)) => T]>; -export function signal(): LuaTuple< - [ - getter: () => T | undefined, - setter: (newValue: T | undefined | ((currentValue?: T) => T | undefined)) => T | undefined, - ] ->; +export function signal(initialValue: T, equals?: Equals): LuaTuple<[getter: Getter, setter: Setter]>; +export function signal(): LuaTuple<[getter: Getter, setter: Setter]>; /** * Creates a reactive atom that acts as both a signal getter and setter. @@ -102,7 +99,7 @@ export function atom(): Atom; * @param getter A function that produces the next value. * @see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter */ -export function computed(getter: (previousValue?: T) => T): () => T; +export function computed(getter: (previousValue?: T) => T): Getter; /** * Creates an effect that runs a callback in response to signal state changes. @@ -205,7 +202,7 @@ export function batch(fn: (...args: Args) => Result, * @returns A function for disposing the effect. * @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback */ -export function listen(getter: () => T, fn: (value: T, previousValue: T | undefined) => void): Cleanup; +export function listen(getter: Getter, fn: (value: T, previousValue: T | undefined) => void): Cleanup; /** * Creates an effect that only runs the callback when the value returned by the @@ -220,7 +217,7 @@ export function listen(getter: () => T, fn: (value: T, previousValue: T | und * @returns A function for disposing the effect. * @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback */ -export function subscribe(getter: () => T, fn: (value: T, previousValue: T) => void): Cleanup; +export function subscribe(getter: Getter, fn: (value: T, previousValue: T) => void): Cleanup; /** * Creates a new read-only signal that is computed by mapping over the entries @@ -232,19 +229,19 @@ export function subscribe(getter: () => T, fn: (value: T, previousValue: T) = * @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper */ export function mapped( - getter: () => Map | ReadonlyMap, + getter: Getter | ReadonlyMap>, mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO, -): () => ReadonlyMap; +): Getter>; // Overload for arrays export function mapped( - getter: () => readonly VI[], + getter: Getter, mapper: (value: VI, key: number) => LuaTuple<[VO, K]> | VO, -): () => K extends number ? readonly VO[] : { readonly [P in K]: VO }; +): Getter; // Overload for objects export function mapped( - getter: () => { readonly [K in KI]: VI }, + getter: Getter<{ readonly [K in KI]: VI }>, mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO, -): () => { readonly [K in KO]: VO }; +): Getter<{ readonly [K in KO]: VO }>; /** * Calls the callback function for each unique key in the table returned by the @@ -263,12 +260,12 @@ export function mapped( * @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback */ export function observe( - getter: () => Map | ReadonlyMap | readonly V[], + getter: Getter | ReadonlyMap | readonly V[]>, fn: (value: V, key: K) => Cleanup | void, ): Cleanup; // Overload for objects export function observe( - getter: () => { readonly [K in Key]: V }, + getter: Getter<{ readonly [K in Key]: V }>, fn: (value: V, key: K) => Cleanup | void, ): Cleanup; diff --git a/packages/charm/src/init.luau b/packages/charm/src/init.luau index 650eced..ac90a3d 100644 --- a/packages/charm/src/init.luau +++ b/packages/charm/src/init.luau @@ -16,9 +16,12 @@ local checkDirty = system.checkDirty local shallowPropagate = system.shallowPropagate local createReactiveSystem = system.createReactiveSystem -type Cleanup = () -> () - -export type Atom = (...((T) -> T) | T) -> T +export type Getter = () -> T +export type Setter = (update: Update) -> T +export type Atom = (...Update) -> T +export type Update = ((current: T) -> T) | T +export type Equals = (current: T, incoming: T) -> boolean +export type Cleanup = () -> () type EffectScopeNode = ReactiveNode & { cleanups: { Cleanup }, @@ -412,10 +415,7 @@ end @return Setter function to update the signal value @see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals ]] -local function signal( - initialValue: T, - equals: ((current: T, incoming: T) -> boolean)? -): (() -> T, (((T) -> T) | T) -> T) +local function signal(initialValue: T, equals: Equals?): (Getter, Setter) local this: SignalNode = { currentValue = initialValue, pendingValue = initialValue, @@ -426,7 +426,7 @@ local function signal( deepFreeze(initialValue) end - local function writeSignal(update: ((T) -> T) | T): T + local function writeSignal(update: Update): T local value: T -- Determine the new value based on whether the update is a function or @@ -505,7 +505,7 @@ end @param equals Optional comparator function to check for value changes @see https://github.com/littensy/charm?tab=readme-ov-file#atominitialValue-equals ]] -local function atom(initialValue: T, equals: ((current: T, incoming: T) -> boolean)?): Atom +local function atom(initialValue: T, equals: Equals?): Atom local get, set = signal(initialValue, equals) local function atomOper(...) return if select("#", ...) == 0 then get() else set((...)) @@ -522,7 +522,7 @@ end @return A function that returns the cached value @see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter ]] -local function computed(getter: (previousValue: T?) -> T): () -> T +local function computed(getter: (previousValue: T?) -> T): Getter local this: ComputedNode = { flags = NONE, getter = noYield(getter), @@ -784,7 +784,7 @@ end @param fn A signal or function that accesses signals @return `fn` wrapped in a computed signal if needed ]] -local function toComputed(fn: () -> T): () -> T +local function toComputed(fn: Getter): Getter local name = debug.info(fn, "n") return if name == "readSignal" or name == "readComputed" or name == "atomOper" then fn else computed(fn) end @@ -802,7 +802,7 @@ end @return A cleanup function that stops the effect @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback ]] -local function listen(getter: () -> T, fn: (state: T, previous: T?) -> ()): Cleanup +local function listen(getter: Getter, fn: (state: T, lastState: T?) -> ()): Cleanup local tracked = toComputed(getter) local current: T @@ -826,7 +826,7 @@ end @return A cleanup function that stops the effect @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback ]] -local function subscribe(getter: () -> T, fn: (state: T, lastState: T) -> ()): Cleanup +local function subscribe(getter: Getter, fn: (state: T, lastState: T) -> ()): Cleanup local tracked = toComputed(getter) local current: T local firstRun = true @@ -851,12 +851,12 @@ end key is removed or the `observe()` effect is disposed. Effects created during the callback will also be bound to the lifetime of the key. - @param getter Function that returns a table + @param getter A signal or function that returns a table @param fn Function that is called for each key added to the table @return A cleanup function that disposes scopes for all keys @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback ]] -local function observe(getter: () -> { [K]: V }, fn: (item: V, key: K) -> ...Cleanup?): Cleanup +local function observe(getter: Getter<{ [K]: V }>, fn: (item: V, key: K) -> ...Cleanup?): Cleanup local innerScopes: { [K]: Cleanup } = {} local disposed = false @@ -907,11 +907,11 @@ end of an existing atom. If the key is omitted from the mapper's return value, the original key is preserved. - @param getter Function that returns a table + @param getter A signal or function that returns a table @param mapper Function that maps each entry from the source to a new entry in the mapped table @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper ]] -local function mapped(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO } +local function mapped(getter: Getter<{ [KI]: VI }>, mapper: (VI, KI) -> (VO, KO)): Getter<{ [KO]: VO }> local currentInput: { [KI]: VI } = {} local keyCache: { [KI]: KO } = {} @@ -984,9 +984,9 @@ return { listen = listen, observe = observe, mapped = mapped :: (( - getter: () -> { [KI]: VI }, - mapper: (item: VI, key: KI) -> (VO, KO) - ) -> () -> { [KO]: VO }) & ((getter: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO }), + getter: Getter<{ [KI]: VI }>, + mapper: (VI, KI) -> (VO, KO) + ) -> Getter<{ [KO]: VO }>) & ((getter: Getter<{ [K]: VI }>, mapper: (VI, K) -> VO) -> Getter<{ [K]: VO }>), onCleanup = onCleanup, getActiveSub = getActiveSub, setActiveSub = setActiveSub,