typescriptsignals

Why does if statement change my type definition?


I'm working on a self-guided exercise to learn about JS signals.

I'm following this blog post, but I'm trying to rewrite the code in TypeScript to better understand it.

While I've managed to get the code running with most of the type definitions, I'm facing an issue with the signal.write function.

Here's a screenshot of the error I'm encountering:

screenshot of the function with the error

TS playground

type Observer = { notify: () => void; link: (unlink: any) => void; }
type Signal = <T>(value:T) => [() => T, (value: T | ((value: T) => T)) => void]

let activeObserver: Observer | null = null

const signal:Signal = <T>(value:T) => {
    let _value: T = value
    const _subscribers: Set<Observer> = new Set()

    function unlink(dep:Observer) {
        _subscribers.delete(dep)
    }

    function read() {
        if (activeObserver && !_subscribers.has(activeObserver)) {
            _subscribers.add(activeObserver)
            activeObserver.link(unlink)
        }
        return _value
    }

    function write(valueOrFn: T | ((value: T) => T)) {
        const newValue:T = typeof valueOrFn === "function" ? valueOrFn(_value) : valueOrFn

        if (newValue === _value) return
        _value = newValue

        for (const subscriber of [..._subscribers]) {
            subscriber.notify()
        }
    }

    return [read, write]
}

const effect = (cb: () => unknown) => {
    let _externalCleanup: unknown // defined explicitly by user
    let _unlinkSubscriptions: Set<(dep:Observer) => void> = new Set() // track active signals (to unlink on re-run)

    const effectInstance = { notify: execute, link }

    function link(unlink: (dep:Observer) => void) {
        _unlinkSubscriptions.add(unlink)
    }

    function execute() {
        dispose()
        activeObserver = effectInstance
        _externalCleanup = cb()
        activeObserver = null
    }

    function dispose() {
        if (typeof _externalCleanup === "function") {
            _externalCleanup()
        }
    }

    execute()

    return dispose
}

let [count, setCount] = signal(0); // state value set to 0

setInterval(
    () => {
        setCount(prev => prev + 1)
    },
    1000
);

effect(() => {
    console.log('Count has changed!', count());
})

Solution

  • You have T as any when narrowing the union you have any & Function which TS can't determine whether it's callable. If you for example change to T extend string (to a non function type). TS would infer string & Function as never and properly discards this union member, leaving only the other callable member. I guess you should rethink what T could be in your code.