luisherranz / deepsignal

DeepSignal 🧶 - Preact signals, but using regular JavaScript objects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Type errors with nested object types

MMMikeM opened this issue · comments

Hi @luisherranz

I'd like to start off by saying I think the API you've written here is absolutely excellent. I've rewritten around 1000 lines of old contexts to deep signals, and everything is way easier to reason with.

I have run into one issue so far, and this might be entirely down to user error. I've narrowed things down to the minimal reproduction below

import { deepSignal } from "deepsignal";

type Example = {
  testKey: Record<string, string | boolean>;
};

const initialState: Example = {
  testKey: { testValue: false },
};

const state = deepSignal<Example>(initialState);

const testKey: Example["testKey"] = { testValue: true };

state.testKey = testKey;

I get the following error:

Type 'Record<string, string | boolean>' is not assignable to type 'DeepSignalObject<Record<string, string | boolean>>'.
  Type 'Record<string, string | boolean>' is not assignable to type '{ [x: `$${string}`]: Signal<string | boolean> | undefined; }'.
    'string' and '`$${string}`' index signatures are incompatible.
      Type 'string | boolean' is not assignable to type 'Signal<string | boolean> | undefined'.
        Type 'string' is not assignable to type 'Signal<string | boolean>'.ts(2322)

I see a similar error with

type Example = {
  testKey: { [key: string]: string | boolean };
};

however if I change the string into a literal,

type Example = {
  testKey: Record<"someLiteral", string | boolean>;
};

or

type Example = {
  testKey: { ["someLiteral"]: string | boolean};
};

I no longer get the error. I have tried various permutations of using optional values / undefined unions, but to no real avail.

Do you see anything here that I'm doing incorrectly here?

Thanks very much!

The problem comes from that fact that deepSignal() modifies the types of the objects to add the $prop props.

const initialState = {
  testKey: { testValue: true },
};

// this is not a deepsignal, $testValue doesn't exist
initialState.testKey.$testValue;

const state = deepSignal(initialState);

// this is a deepsignal, $testValue exists
state.testKey.$testValue;

You are trying to use a type that doesn't have those $prop props in an object that is already a deep signal and therefore needs those $prop props.

const testKey: Example["testKey"] = { testValue: true }; // this type is not a deep signal

state.testKey = testKey; // but now you are assigning it to a deep signal!

TypeScript can't convert types on assignment, so you have two options:

  • Transform the type into a deep signal before the assignment:

    import { DeepSignal } from "deepsignal";
    
    const testKey: DeepSignal<Example["testKey"]> = { testValue: true }; // this type is a deep signal
    
    state.testKey = testKey; // now TypeScript doesn't complain

    This is technically not 100% correct because the testKey variable now has testKey.$testValue.

  • Change the type on the assignment itself:

    import { DeepSignal } from "deepsignal";
    
    const testKey: Example["testKey"] = { testValue: true };
    
    state.testKey = testKey as DeepSignal<typeof testKey>;

Hope this helps!

I'm closing this now as solved, but feel free to reopen it if you have more related questions.