Skip to content

A powerful writable derived store for Svelte that enables deep object and array manipulation with TypeScript support

License

Notifications You must be signed in to change notification settings

humanspeak/svelte-keyed

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

@humanspeak/svelte-keyed

svelte-keyed-banner

A powerful TypeScript-first derived store for Svelte 5 that enables deep object and array manipulation with full reactivity.

npm version build coverage Downloads License CodeQL Install size Code Style: Trunk

Features

  • ๐ŸŽฏ Type-Safe: Full TypeScript support with automatic type inference
  • ๐Ÿ”„ Reactive: Deep object and array manipulation with automatic updates
  • ๐ŸŽจ Svelte 5 Ready: Built for the latest Svelte features
  • ๐Ÿชถ Lightweight: Zero dependencies, tiny bundle size
  • ๐Ÿ”’ Null-Safe: Built-in handling for nullable types
  • ๐ŸŽฎ Easy API: Simple dot notation for deep object access

Quick Start

const user = writable({ name: { first: 'Rich', last: 'Harris' } })
const firstName = keyed(user, 'name.first')

$firstName = 'Bryan'

console.log($user) // { name: { first: 'Bryan', last: 'Harris' } };

Installation

npm i -D svelte-keyed

Since Svelte automatically bundles all required dependencies, you only need to install this package as a dev dependency with the -D flag.

Why svelte-keyed?

While Svelte's built-in stores are powerful, they don't provide an elegant way to work with nested properties. svelte-keyed solves this by:

  • Enabling direct manipulation of nested properties
  • Maintaining full TypeScript support
  • Providing a clean API for complex state management
  • Supporting both object and array access patterns

API

keyed takes a writable object store and a keypath, and returns a writable store whose changes are reflected on the original store.

Properties are accessed with dot notation, and arrays can be indexed with bracket notation.

const email = keyed(settings, 'profiles[0].email')

Nullable parents

If the parent store is nullable, then the child store will also be nullable.

type User = {
    name: {
        first: string
        last: string
    }
    relations: {
        partner?: User
    }
}

const maybeUser = writable<User | undefined>(undefined)
// Writable<string | undefined>
const firstName = keyed(maybeUser, 'name.first')

Nullable properties

Nullable properties are accessed with optional chaining behaviour.

const user = writable(initUser)
// Writable<Name | undefined>
const partnerName = keyed(user, 'relations.partner.name')

TypeScript

keyed infers the return type of the keyed store from the keypath.

const user = writable(initUser)
// Writable<string>
const firstName = keyed(user, 'name.first')

keyed will also try to guess all possible keypaths up to a depth limit of 3.

keyed(user, '...');
            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
            โ”‚ โ€ข name                        โ”‚
            โ”‚ โ€ข name.first                  โ”‚
            โ”‚ โ€ข name.last                   โ”‚
            โ”‚ โ€ข relations                   โ”‚
            โ”‚ โ€ข relations.partner           โ”‚
            โ”‚ โ€ข relations.partner.name      โ”‚
            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

This limit is due to a TypeScript limitation where structured types must be generated statically. Increasing the depth limit slows down type compilation.

Type hints will not be provided for keypaths with a depth greater than 3 but this does not affect the return type.

const user = writable(user)
// Writable<string | undefined>
const firstName = keyed(user, 'relations.partner.name.first')

Motivations

We usually read and write properties of an object store with auto-subscriptions.

<input bind:value={$name.first} />

However, auto-subscriptions are isolated to a Svelte component. svelte-keyed aims to solve several common limitations listed below.

Context stores

Often, we want to set a property or element of a store into component context, then allow child components to read / write to the property.

<!-- Settings.svelte -->
<script>
    setContext('profileSettings', keyed(settings, 'profile'))
</script>

<GeneralSettings />
<ProfileSettings />
<!-- ProfileSettings.svelte -->
<script>
    const profileSettings = getContext('profileSettings')
</script>

<input type="text" bind:value={$profileSettings.username} />

Helper functions

One important method to reduce clutter on your component is to extract functionality into external helper functions. svelte-keyed allows you to create derived Writable stores that can be passed into or returned from helper functions.

<!-- Settings.svelte -->
<script>
    const stats = writable({ userClicks: 0, userTaps: 0 })
    const clicks = keyed(stats, 'userClicks')
</script>

<div use:trackClicks={clicks} />
<input use:trackClicks={clicks} />
export const trackClicks = (node, clicks) => {
    const listen = () => {
        clicks.update(($clicks) => $clicks + 1)
    }
    node.addEventListener('click', listen)
    return {
        destroy() {
            node.removeEventListener('click', listen)
        }
    }
}

License

MIT ยฉ Humanspeak, Inc.

Credits

Made with โ™ฅ by Humanspeak