Search code examples
typescriptsveltesveltekit

in a Svelte Writable store, how can I set a different Type for getter/setter in Typescript/Svelte?


In one of my projects, I am using SvelteKit, Tailwind CSS, and especially TypeScript.

During the development, I understood that I must use a writable store, instead of passing the props.

Everything works fine, but the typing system is not as dynamic as it should be.


Let me explain,
To make you understand, where I want to go,
By oversimplifying my actual scenario:

I have an array with child objects with the same shape, like this:

[
  {
    id: "myUniqueId",
    type: "MyType",
    properties: {
      foo: "bar",
    },
  },
  {
    ...
  },
  {
    ...
  },
];

this array is passed inside the writable store like this:

import { writable } from "svelte/store";
import { exampleArray } from "$lib/data/exampleData";
export const myAppStore = writable<TypeMyAppStore>([]);

myAppStore.set(exampleArray);

with this code, everything is right, even in typescript

but, now I want to make the user to explicitly,
to don't add the id, so the object becomes something like this:

{
  type: "MyType",
  properties: {
    foo: "bar",
  },
},

the id will be added dynamically using .set() and .update()

appStore.subscribe((value) => { 
  value.forEach((thisObj) => {
    thisObj.id = generateUniqueId();
  });
  appStore.set(value);
});

with this approach, I can basically add the IDs dynamically when the user pushes a new object inside my store.

the problem is in the IntelliSense autocompletion and typescript, because:

  • the parameter input of the writable store, should NOT have id
  • But the store itself, must have a id (a string)

the code and app is running fine,
but I need a way to make the writable store, accept only type without id, but in intellisense suggest the id.

if you think about it, we can say that:

  • the "getter" should make the typescript suggest the type that includes id,
  • the "setter" must accept only an array of objects that omit id.

if we want to talk about the same situation but in another context, is like this:

function foo(input: TypeMyInput): TypeMyOuput {}
//This is literally what I want to implement in my svelte store,
//Accept one input with a certain type
// change that array using `.subscribe()`/`.set()`
// then make by svelte store, to think about another type (and not give errors anymore only if the type is not respected, and also give me autocompletion and intellisense)

//For now I solved only the input, but the output not.
// writable<TypeMyInput>([])

How we can do both (I mean without union type) in a way that can accept only one type, but return another type?


all the code above is not nearly my real-world code, is an oversimplified example for you.

Remember

  • I am not asking about how I write the types (since I have experience in typescript/svelte-kit),
    But how I can make the svelte store writable and do the things I wrote before,
    Because is something new that isn't written in the docs,
    So I hope someone who had the same situation as me, answers this question.
  • the code execution is right and working fine, don't focus on that (I mean all suggestions for improvements are accepted), but the problem is only inside the typescript part.

I hope you understand my situation.

if you still don't understand what I want, I can give you the link to my GitHub repo https://github.com/Laaouatni/laaCAD/blob/development/app/src/lib/Data/appStore/appStore.ts


Solution

  • You might want to look at custom stores these allow you to create your own stores (that you can subscribe to with $) while adding some extra functionality.

    For example this code

    import { writable } from 'svelte/store';
    
    const createStore = <T extends { id: string }>() => {
      const store = writable<T[]>([]);
    
      return {
        subscribe: store.subscribe,
        add: (value: Exclude<T, 'id'>) => {
          const value_with_id = {
            ...value,
            id: crypto.randomUUID() as string
          };
          store.update(($store) => {
            $store.push(value_with_id);
            return $store;
          });
        }
      };
    };
    
    export default createStore;
    

    does almost what you want:

    • define a store of items (with id) for which the subscribed value included the id
    • define an "add" method that forces you to only add items without id

    I made it with an add function just because it feels weird that everytime you change the store you would change all ids ? (I wonder if this wouldn't also cause an infinite loop for that matter because you are changing all items everything an item changes?) You also ask for a setter without id, but then use .set with values that have an id, which seems a bit strange :)