Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

Monday, January 15, 2024

Zustand, devtools and immer wrapper, pt. 2

On further researching of my previous idea of creating a Zustand + Immer wrapper, I found a very interesting technique straight from Zustand docs, where all the methods are simply free functions declared outside the store, called no store actions. I was already using this to declare private store methods, but it never occurred me to use it for all methods.

Since it officially has no downsides, I applied this idea to append the actions directly to the hook itself, so we don’t need to import each free function. The problem is that the hook has a couple methods itself, plus the inherent ones to any function object. So the result is very polluted. So I decided to adopt a convention of prefixing every action with $.

But conventions are easy to break. So I implemented a reduce to prefix each method name with #. But then TypeScript could not infer the type anymore, so I had to dig and type it manually with key remapping, and finally I got this implementation:

import {create, StateCreator} from 'zustand';
import {devtools} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';
	
export function createStore<T extends object, U extends object>(
	name: string,
	initialState: T,
	actions: StateCreator<
		T,
		[['zustand/devtools', never], ['zustand/immer', never]],
		[['zustand/immer', never], ['zustand/devtools', never]],
		U
	>,
) {
	const store = create(
		devtools(
			immer(() => initialState),
			{name: name, serialize: true},
		),
	);

	type WithPrefix<T> = {
		[K in keyof T as K extends string ? `$${K}` : never]: T[K];
	}
	const actionsOrig = actions(store.setState, store.getState, store);
	const actionsPrefixed = Object.entries(actionsOrig).reduce(
		(accum, [k, v]) => ({...accum, ['$' + k]: v}),
		{} as WithPrefix<typeof actionsOrig>,
	);

	return Object.assign(store, actionsPrefixed);
}

This creator allows us to call actions directly, without the hook. They’ll be automatically prefixed with $:

const useFoo = createStore('Foo', {
	name: '',
}, (set, get) => ({
	setName(name: string): void {
		set(state => {
			state.name = name;
		});
	},
}));
	
function App() {
	const name = useFoo(s => s.name);

	return <div>
		<span>{name}</span>
		<button onClick={() => useFoo.$setName('Joe')}</button>
			Change
		</button>
	</div>;
}

That $ prefix gives me bad Vue vibes. Let’s see if I can get along with it, because I was able to develop a DX which is exactly what I was looking for.

Wednesday, November 29, 2023

Setting/resetting Vue reactive objects

Vue 3 has many idiosyncrasies, among them the overlapping ref and reactive constructs. One of the main differences is that reactive content cannot be replaced. One StackOverflow answer proposes using Object.assign, but it will replace all nested references, losing them all.

I ended up writing my own version of a recursive function to replace each value inside an object, so I won’t need to deal with the muddy details ever again:

/**
 * Recursively copies each field from src to dest, avoiding the loss of
 * reactivity. Used to copy values from an ordinary object to a reactive object.
 */
export function deepAssign<T extends object>(destObj: T, srcObj: T): void {
	const dest = destObj;
	const src = toRaw(srcObj);
	if (src instanceof Date) {
		throw new Error('[deepAssign] Dates must be copied manually.');
	} else if (Array.isArray(src)) {
		for (let i = 0; i < src.length; ++i) {
			if (src[i] === null) {
				(dest as any)[i] = null;
			} else if (src[i] instanceof Date) {
				(dest as any)[i] = new Date(src[i].getTime());
			} else if (Array.isArray(src[i])
					|| typeof src[i] === 'object') {
				deepAssign((dest as any)[i], src[i]);
			} else {
				(dest as any)[i] = toRaw(src[i]);
			}
		}
	} else if (typeof src === 'object') {
		for (const k in src) {
			if (src[k] === null) {
				(dest as any)[k] = null;
			} else if (src[k] instanceof Date) {
				(dest[k] as any) = new Date((src[k] as any).getTime());
			} else if (Array.isArray(src[k])
					|| typeof src[k] === 'object') {
				deepAssign(dest[k] as any, src[k] as any);
			} else {
				(dest[k] as any) = toRaw(src[k]);
			}
		}
	} else {
		throw new Error('[deepAssign] Unknown type: ' + (typeof src));
	}
}

Another problem with reactive is that, to avoid two variables pointing to the same point, we must deep clone an object when creating the object. I also wrote a function to deal with that:

/**
 * Deeply clones an object, eliminating common references. Used to create a
 * reactive object by copying from an ordinary object.
 */
export function deepClone<T>(origObj: T): T {
	const obj = toRaw(origObj);
	if (obj === undefined
			|| obj === null
			|| typeof obj === 'string'
			|| typeof obj === 'number'
			|| typeof obj === 'boolean') {
		return obj;
	} else if (Array.isArray(obj)) {
		return obj.reduce((acum, item) => [...acum, deepClone(item)], []);
	} else if (obj instanceof Date) {
		return new Date(obj.getTime()) as unknown as T;
	} else if (typeof obj === 'object') {
		return Object.entries(obj).reduce(
			(acum, [key, val]) => ({...acum, [key]: deepClone(val)}), {}) as T;
	} else {
		throw new Error('[deepClone] Tipo desconhecido: ' + (typeof obj));
	}
}

I remember from my MobX days some people saying that working with reactive variables can be tricky. Now I can clearly see why.

Tuesday, November 21, 2023

Zustand, devtools and immer wrapper

I’m considering a Vue 3 to React migration at work, and while writing a proof of concept, I’m standardizing the Zustand stores creation, which will all use devtools and immer middlewares. And this results in convoluted and error-prone code, which I’m trying to abstract away with a generator function. Such a function, however, has proven to be very hard to write with perfect TypeScript typings.

I had to resort to the great Daishi Kato again, which very patiently helped me to write. It indeed has a deep level of TypeScript black magic which I’m not familiar with:

import {create, StateCreator} from 'zustand';
import {devtools} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';

export function createFullStore<T extends object, U extends object>(
	name: string,
	state: T,
	actions: StateCreator<
		T,
		[['zustand/devtools', never], ['zustand/immer', never]],
		[['zustand/immer', never], ['zustand/devtools', never]],
		U
	>,
) {
	return create<T & U>()(
		devtools(
			immer((...a) => Object.assign({}, state, (actions as any)(...a))),
			{name},
		),
	);
}

It works perfectly, with all the type inferences one would expect. I’m really thankful to the guy.

By the way this should’ve been published yesterday, but after a whole day without internet at home, I finally can do it.

Monday, November 13, 2023

TypeScript function to set value by key

While researching React stores automation, I wondered whether it would be possible to have a TypeScript function to set the value of an object by the name of its property, with correctly typed parameters, of course.

Turns out, it is

export function setField<
	T extends object,
	K extends keyof T,
>(o: T, k: K, v: T[K]): void {
	o[k] = v;
}

The function above works perfectly with interfaces:

interface Person {
	name: string;
	age: number;
}

const p: Person = { name: 'Joe', age: 42 };
setField(p, 'name', 'foo');

Saturday, November 11, 2023

Jotai utilities for read-only and write-only atoms

Three months ago I wrote an utility function to deal with write-only atoms in Jotai. To go with it, today I wrote a function to deal with read-only atoms, so now I have:

import {Atom, useAtom, WritableAtom} from 'jotai';

/**
 * Syntactic sugar to useAtom() for read-only atoms.
 */
export function useReadAtom<V>(a: Atom<V>) {
	const [val] = useAtom(a);
	return val;
}

/**
 * Syntactic sugar to useAtom() for write-only atoms.
 */
export function useWriteAtom<V, A extends unknown[], R>(a: WritableAtom<V, A, R>) {
	const [_, setFunc] = useAtom(a);
	return setFunc;
}

It’s worth mentioning that useReadAtom also works for derived atoms, used for computed values.

Friday, November 10, 2023

Zustand computed values, pt. 2

I’ve been trying to find a way for computed values in Zustand for a while. The general recommended way is using a custom hook, according to Daishi Kato himself. The issue with this approach, however, is that your computed logic will be completely alienated from the store itself.

I just found a neat approach which seems to work: using a nested object inside the store itself, as pointed in this highly cheered comment. Below is a fully typed example:

import {create} from 'zustand';
import {combine} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';

export interface Person {
	name: string;
	age: number;
}

const usePeople = create(immer(
	combine({
		people: [] as Person[],
	},
	(set, get) => ({
		$computed: {
			get count(): number {
				return get().people.length;
			},
		},
		addNew(name: string, age: number): void {
			set(state => {
				state.people.push({name, age});
			});
		},
	})),
));

export default usePeople;

The tradeoff, as benchmarked by myself, is that this $computed getters are not cached and will run every time the state changes, in addition to every time they are requested – which happens when the component re-renders. The ordinary custom hook approach runs only when they are requested. So, there is a performance penalty, which can be huge if the logic is too demanding.

Saturday, August 19, 2023

Calling write-only atoms in Jotai

Every atom in Jotai, when called through its useAtom primitive, returns a tuple of value + setter, just like React’s own useState. In a Jotai write-only atom, however, there is no value, so the canonical call is:

const [_, setVal] = useAtom(myValAtom);

In order to get rid of the verbose underscore, and thus the whole useless tuple, I wrote the following function:

import {WritableAtom} from 'jotai';

/**
 * Syntactic sugar to useAtom() for write-only atoms.
 */
export function useWriteAtom<V, A extends unknown[], R>(a: WritableAtom<V, A, R>) {
	const [_, setFunc] = useAtom(a);
	return setFunc;
}

The atom type was copied straight from Jotai’s TypeScript definitions, so the type inference works seamlessly. The example now can be written as:

const setVal = useWriteAtom(myValAtom);

Monday, June 26, 2023

A generic hook for useState and Immer

While prospecting the use of useState with the great Immer, I ended up finding the small use-immer package, which unifies both things. However, it doesn’t have proper TypeScript typings.

So I wrote my own hook to perform this task: just like useState, it returns a value and a setter, but the setter’s argument is a mutable state, automatically wired to produce, and all that using a generic argument for proper typing:

import {useCallback, useState} from 'react';
import {Draft, produce} from 'immer';

export function useStateImmer<T>(initialState: T | (() => T)): [
	T,
	(updater: (draft: Draft<T>) => void) => void,
] {
	const [obj, setObj] = useState(initialState);
	const setObjProduce = useCallback((updater: (draft: Draft<T>) => void): void => {
		setObj(curState => {
			return produce(curState, draft => {
				updater(draft);
			});
		});
	}, [setObj]);
	return [obj, setObjProduce];
}

Note that the returned setter function is identity, since it’s guarded by an useCallback call.

Thursday, June 8, 2023

Using Zustand, TypeScript and Immer together

In React land, Redux nightmare finally came to an end with the inception of Zustand, which throws away all the chores we had to previously do, leaving us with a simple workflow of minimal code. On top of that, TypeScript allows us to relax our brains with some degree of strong typing.

When dealing with our Zustand store, just like Redux, we have to manipulate a chunk of immutable data, which becomes more and more annoying as we deepen the object structure. And, when it comes to dealing immutable data, Immer is everyone’s best friend – just slap a produce call and you can go home earlier.

Now here comes the question: how to use Immer inside our Zustand store?

Turns out Zustand provides a few middlewares – functions which lie between the data and your manipulation functions –, and among them an Immer middleware. Using the middleware has two advantages:

  • you don’t need to explicitly call produce, because the state argument already comes to you as a WritableDraft;
  • you don’t need to explicitly type the state argument, because it already comes to you properly typed.

To put everything together, with automatic type inference, we also need the combine middleware, so we don’t need to write the store type by hand. Here is the full template that can be used as the starting point of your store:

import {create} from 'zustand';
import {combine} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';

interface Person {
	name: string;
	age: number;
}

const useStore = create(immer(
	combine({
		people: [] as Person[],
	},
	(set, get) => ({
		add(name: string, age: number) {
			set(state => {
				state.people.push({name, age});
			});
		},
		remove(name: string) {
			set(state => {
				state.people = state.people.filter(p => p.name === name);
			});
		},
	})),
));

export default useStore;

Notice how you can simply mutate the state inside the actions.

Saturday, June 3, 2023

TypeScript map for objects

In another round of React experiments, after another Vue frustration with Volar not typing event arguments, I was again into the state normalization land. When dealing with objects with IDs as keys, every operation requires a tedious reduce, in contrast to the intuitive map we use with arrays.

So I wrote a map for objects:

function mapObj<T>(obj: Record<string, T>, callback: (elem: T, key: string) => T): Record<string, T> {
	return Object.keys(obj).reduce((accum, key) => {
		accum[key] = callback(obj[key], key);
		return accum;
	}, {} as Record<string, T>);
}

It’s fully typed, and it was surprisingly easy to write. TypeScript is a great language.

Thursday, February 9, 2023

Fetch calls with global Jotai object

Zustand is a great React state management library, but it has the drawback of not having a standard way to define getters, which must be defined as ordinary hooks.

Written by the same author, Jotai seems to be the answer to this situation. However, while Zustand provides a way to change the state outside React components, Jotai does not. Today I was able to devise a way to work around this, by simply applying the hooks concept, answering my own question:

const errorAtom = atom(''); // storage

const errorReadAtom = atom(get => { // read-only (computed)
	return 'Error: ' + get(errorAtom);
});

const writeErrorAtom = atom(null, (get, set, arg: string): void => { // setter
	set(errorAtom, arg);
});

function useGet(): (url: string) => Promise {
	const [, setError] = useAtom(errorAtom);
	return async (url: string): Promise => {
		const resp = await fetch(url);
		const json = await resp.json();
		setError('Hello');
		return json as T;
	};
}

Usage:

function App() {
	const doGet = useGet();

	async function click(): void {
		const data = await doGet('/foo');
	}

	return <button onClick={click}>Click</button>;
}

So, with Jotai I finally have the complete solution to state management in React.

Wednesday, October 5, 2022

Zustand computed values

This week I’ve faced some situations with Vue’s reactive that alarmed me. I’m finding the hard way what I’ve read a couple times: reactive proxies can behave unpredictably in some situations. Too bad I’m in the middle of a project which began in Vue 3. I’m strongly considering rewriting it in React now – yes, it will be an insane amount of work, including back-end changes to return normalized objects.

But React brings its own problems. The biggest of all is certainly the raw state management.

Among all state management tools I’m evaluating, Zustand is showing to be the most promising. It’s ticking all the boxes, and the only open question so far is computed state. The best I could do was to use custom hooks, but they look rather ugly and verbose:

import create from 'zustand';
import {combine} from 'zustand/middleware';

/**
 * The store, which holds the state and the actions.
 */
const useBearStore = create(
	combine({
		bears: 0,
	},
	(set, get) => ({
		increasePopulation(): void {
			set(state => ({ bears: state.bears + 1 }));
		},
		removeAllBears(): void {
			set({ bears: 0 });
		},
	})),
);

export default useBearStore;

/**
 * Custom hook that returns a computed value.
 */
export function useBearCountPlusOne(): number {
	const bears = useBearStore(s => s.bears);
	return bears + 1;
}

Usage example in a component:

import useBearStore, {useBearCountPlusOne} from './useBearStore';

function App() {
	const increasePopulation = useBearStore(s => s.increasePopulation);
	const populationPlusOne = useBearCountPlusOne();

	return <>
		<div>Population + 1: {populationPlusOne}</div>
		<button onClick={() => increasePopulation()}>
			Increase population
		</button>
	</>;
}

If I find a way to rework these loose hooks, I think I found my way.

Wednesday, September 28, 2022

Multiple className values in React

React offers basically zero support for CSS. Personally I’m fond of CSS Modules along with SCSS instead of the slow CSS-in-JS solutions out there. Still, we need a way to manage multiple classes in className property.

In order to mitigate this problem, I wrote a TypeScript function to deal with sequential or conditional class names situations:

/**
 * Generates the className attribute with the given class names.
 */
export function cls(...names: (string | undefined)[]): string;
export function cls(names: (string | undefined)[]): string;
export function cls(names: Record<string, boolean>): string;
export function cls(names: any): string {
	if (typeof names === 'string' && arguments.length === 1) { // just 1 string
		return names === undefined ? '' : names;
	} else if (typeof names === 'string' && arguments.length > 1) { // multiple strings
		let s = '';
		for (const name of arguments) {
			if (name !== undefined) s += name + ' ';
		}
		return s;
	} else if (Array.isArray(names)) { // string array
		let s = '';
		for (const name of names) {
			if (name !== undefined) s += name + ' ';
		}
		return s;
	} else if (typeof names === 'object') {
		let s = '';
		for (const name in names) {
			if (names[name]) s += name + ' ';
		}
		return s
	} else {
		throw 'Invalid class name input!';
	}
}

Example using the 4 argument possibilities:

import {cls} from '@/funcs';
import c from './App.module.scss';
	
function App() {
	return <>
		<div className={cls( c.first )} />
		<div className={cls( c.first, c.second )} />
		<div className={cls( [c.first, c.second] )} />
		<div className={cls({
			[c.first]: true,
			[c.second]: false,
		})} />
	</>;
}

This covers all situations I ever faced.

The spread operator version was added in February 9, 2023.

Tuesday, July 19, 2022

Default props in React function components

Having default props in a React component is a rather common situation. The most popular way to accomplish this is to pass the default values to a defaultProps property on the function component. However, this property will be deprecated in the future.

Spoiler: due to the sheer amount of code written with it, it never will be deprecated. It’s more likely that a warning will show in the console.

Anyway, in order to keep things clean and guard from this future warning, I came up with a clean pure TypeScript solution to this problem:

interface Props {
	name: string;
	surname?: string;
	age?: number;
}

const defaultProps = {
	surname: 'Doe',
};

function MyComponent(propsNonDef: Props) {
	const props = {...defaultProps, ...propsNonDef};

	return <div>{props.surname}</div>;
}

The code above provides the correct behavior and proper TypeScript validation. It ended becoming an answer on StackOverflow.

Thursday, July 7, 2022

Extending built-in TypeScript objects in Vite

Checking whether an array is empty is a rather common operation. The usual way to do this in TypeScript is by checking the length property, but it’s very verbose:

if (myArray.length === 0) {
	// ...
}

For some reason, the JavaScript standard doesn’t specify an isEmpty() method. Fortunately, we can extend native JavaScript objects to add any methods we want. While this is a great feature, I was unable to make it work in a Vue + TypeScript + Vite project. I want the autocomplete of my VSCode to properly display it.

To make the TypeScript compiler recognize it, you must declare your method extensions as global by wrapping the declaration in a declare global block.

At first I tried to write the declaration in the “src/env.d.ts” file, but it didn’t work. Turns out you need to place them in a different file, as explained here. So I finally wrote them in a “src/extensions.d.ts” for the typings, and then the implementations themselves must be in a file that will be actually called in your application:

I tested the code above in a Vue project, but I believe it will work accordingly in a React project as well.

Wednesday, June 22, 2022

Checking if user passed a slot in Vue

In Vue 3 with the Composition API, there is no this.$slot entry to programmatically poke on the slots. You must summon the slots by calling useSlots() – yes, that’s a React hook right there.

The returned object has one entry for each slot. If your component has only one unnamed slot, it will be named 'default'. So, in order to check whether the user didn’t pass the slot, you simply check whether the entry exists:

<script setup lang="ts">
import {computed, useSlots} from 'vue';

const slots = useSlots();
const hasSlot = computed(() => slots['default'] !== undefined);
</script>

Tuesday, June 21, 2022

Global useState hooks with Jotai

I’ve been stressing out several React global state libraries in the past months. Last week it was Jotai’s time.

I liked the concept of “atoms” and how they feel like autonomous useState parts, and how they can be share state among components. I remember trying to write something like this in the past. Jotai seems to be what I tried to do back then:

import {atom} from 'jotai';

export const nameAtom = atom('hello');

Pretty much a global useState here, which is great:

import {useAtom} from 'jotai';
import {nameAtom} from './state';
	
function App() {
	const [name, setName] = useAtom(nameAtom);

	//...
}

In large, real-world applications you’d like to write mutation methods to implement specific logic, rather than having them scattered over the components. Thus we should not make setName public; instead we should provide more specific methods.

In Jotai, while reading the state is trivial, I found writing mutations to be rather cumbersome. The syntax of “writing atoms” is, to my tired eyes, very convoluted:

export const setSurnameAtom = atom(null, (get, set, surname) => {
	set(nameAtom, get(nameAtom) + ' ' + surname);
});

//...

function App() {
	const [, setSurname] = useAtom(setSurnameAtom);
}

After giving it some thought, it occurred me that since useAtom is a hook, I can compose a custom hook over it. And then it all clicked:

export function useName() {
	const [name, setName] = useAtom(nameAtom);
	return useMemo(() => ({
		value: name,
		setSurname(surname: string) {
			setName(name + ' ' + surname);
		},
	}), [name, setName]);
}

Usage of this custom hook is straightforward, crystal clear:

import {useName} from './state';
	
function App() {
	const name = useName();

	return <>
		<h1>{name.value}</h1>
		<button onClick={() => name.setSurname('foo')}>
			Set surname
		</button>
	</>;
}

This is a truly global custom useState hook. This works amazingly well with VSCode autocomplete. This is easy to read. This is beautiful.

In the custom hook above, note the use of useMemo. It cuts down a lot of the processing in inside the custom hook, and it was an insight I had after briefly talking to Daishi Kato himself, the author of Jotai, about this custom hook idea. The future useEvent hook will optimize the button call a little further.

Jotai, unfortunately, has a huge drawback of not allowing accessing atoms outside a React component.

Wednesday, June 1, 2022

React high-order useEffect wrappers

As I was increasingly upset with the lack of variable highlighting in Volar, I started doing some React experiments.

In particular, I found the useEffects hook API very annoying, because it fails to communicate the intent of what you’re trying to accomplish.

So, I order to mitigate this, I wrote a few wrappers:

import {DependencyList, useEffect} from 'react';

function onMount(fun: () => void) {
	useEffect(() => { fun(); }, []);
}

function onUmnount(fun: () => void) {
	useEffect(() => {
		return () => { fun(); };
	}, []);
}

function onMutate(dep: DependencyList, fun: () => void) {
	useEffect(() => { fun(); }, dep);
}

As far as I could test, they work really well, with the advantage that you can pass async functions to them:

function Foo() {
	const [name, setName] = useState('');

	onMount(async () => {
		console.log('hello async');
	});

	onUmnount(() => {
		console.log('unmounted');
	});

	onMutate([name], () => {
		console.log('updated', name);
	});

	return <div>Hello {name}</div>;
}

I’m somewhat tempted to create a library for this. And maybe other high-order wrappers.

Friday, April 22, 2022

Generating React object keys with WeakMap

When iterating through an array in React, a key attribute is expected on the rendered elements, so a reordering is properly rendered. However, often the objects we’re rendering have no unique ID and using the plain index will give us a broken rendering when a reorder happens. So what should we use?

My first idea was to use the object itself as the key, but it must be a string or a number. Then, while researching the matter, I found a rather good solution: using a WeakMap object. I wasn’t even aware that such WeakMap existed, and turns out it’s perfect for the job.

A WeakMap is basically a Map which uses objects as keys. The difference from an ordinary Map is that the Map would retain the objects indefinitely – they would simply pile up, what can be seen as a memory leak –, while the WeakMap lets the objects being garbage collected when they are no longer referenced anywhere.

Given that concept imagine the following interface:

interface Person {
	name: string;
	age: number;
}

Now we have a React component which needs to render an array of Person. This is how we can write it:

interface Props {
	people: Person[];
}
	
function ThePeople(props: Props) {
	return <>
		{props.people.map(person =>
			<div key={getId(person)}>
				{person.name}, {person.age}
			</div>
		)}
	</>;
}

Note the getId function in the code above, which somewhat returns an unique ID for the object.

We’ll use a WeakMap to store the Person objects along with an auto-generated number, which will be its unique ID::

let currentId = 0;
let ids = new WeakMap<Object, number>();

export function getId(obj: Object): number {
	if (ids.has(obj)) {
		return ids.get(obj)!;
	} else {
		const newId = ++currentId;
		ids.set(obj, newId);
		return newId;
	}
}

For each object, the ID is set once, and it can be retrieved any number of times. This effectively eliminates the need of an alien _id field in our struct, and it also prevents the memory leaking of using an ordinary Map.

However, when using immutable state – which is basically the norm in React –, you’ll always have different objects, thus different IDs, and this will cause the loss of focus on elements. So, despite its ugliness, an _id attribute is still better. Or, if the list element won’t reorder, a simple index.

Monday, January 17, 2022

Filtering Pinia store fields

I’m having great joy working with Pinia, so much I’m introducing it at work. It seems reliable so far.

I found a quirk that’s annoying, however. In VS Code autocomplete, all the contents of the store are exposed – that includes all the internals we should not see.

After digging into the code and a lot of experimentation, I finally found a way to filter the visible fields using TypeScript:

import {defineStore, StoreActions, StoreGetters, StoreState} from 'pinia';
	
const useDef = defineStore('main', () => {
	return {};
});

export type ReachableStore =
	StoreActions<ReturnType<typeof useDef>> &
	StoreGetters<ReturnType<typeof useDef>> &
	StoreState<ReturnType<typeof useDef>>;
const useStore = (): ReachableStore => useDef();
export default useStore;

Although those types are exported by Pinia, they have basically zero documentation. I’m considering contribute with some documentation improvement, but I’m unsure if it’s worth.