Have you ever used a JavaScript object like a dictionary to store key-value pairs? Most of us do it all the time, it’s one of the most common patterns in JavaScript. But there’s a hidden problem that can cause unexpected bugs, especially in web applications where data comes from users, URLs, or external APIs.

In this article, you’ll learn about a simple technique called Object.create(null) that creates truly empty objects, why it matters for your Svelte 5 and SvelteKit projects, and see 8 real-world examples where this pattern can save you from subtle bugs.


What’s the Problem with Regular Objects?

When you create an object using the familiar curly braces {}, you might think you’re creating an empty container. But you’re not! Every object in JavaScript secretly inherits properties from something called Object.prototype.

Think of it like this: when you buy an “empty” folder, it might still have a label, a pocket, or other features built into it. Similarly, an “empty” JavaScript object comes with built-in properties.

Let’s see this in action:

// You might think this creates an empty object...
const myObject = {}

// But it actually has hidden properties!
console.log(myObject.toString) // [Function: toString]
console.log(myObject.constructor) // [Function: Object]
console.log(myObject.hasOwnProperty) // [Function: hasOwnProperty]
console.log('__proto__' in myObject) // true

// These properties exist even though we never added them!

Why Does This Matter?

In most cases, these inherited properties don’t cause problems. But imagine you’re building a blog where users can create posts with any URL slug they want. What happens if someone creates a post with the slug “constructor”?

// A simple cache for blog posts
const postCache = {}

// Store some posts
postCache['hello-world'] = { title: 'Hello World', content: '...' }
postCache['my-first-post'] = { title: 'My First Post', content: '...' }

// Now someone creates a post with the slug "constructor"
console.log(postCache['constructor'])
// Output: [Function: Object] - NOT undefined!

// The cache thinks there's already a post here because
// "constructor" is an inherited property!

This is called prototype pollution, and it can lead to:

  • Bugs: Your code behaves unexpectedly when certain keys are used
  • Security issues: Attackers can exploit these inherited properties
  • Hard-to-find errors: These bugs only appear with specific key names

The Solution: Object.create(null)

JavaScript gives us a way to create a truly empty object with no inherited properties at all. The syntax is Object.create(null):

// This creates a TRULY empty object
const cleanObject = Object.create(null)

// No inherited properties!
console.log(cleanObject.toString) // undefined
console.log(cleanObject.constructor) // undefined
console.log(cleanObject.hasOwnProperty) // undefined
console.log('__proto__' in cleanObject) // false

// Now we can safely use any key
cleanObject['constructor'] = { title: 'Constructor Pattern', content: '...' }
console.log(cleanObject['constructor']) // { title: 'Constructor Pattern', content: '...' }
// It works correctly!

How Does It Work?

The Object.create() method creates a new object and lets you specify what its prototype should be. When you pass null, you’re saying “this object should have no prototype at all” - making it a completely blank slate.

// Regular object: has Object.prototype as its prototype
const regular = {}
console.log(Object.getPrototypeOf(regular)) // [Object: null prototype] {}

// Null-prototype object: has no prototype
const empty = Object.create(null)
console.log(Object.getPrototypeOf(empty)) // null

When Should You Use This Pattern?

Here’s a simple rule of thumb:

Use Object.create(null) when:

  • Keys come from user input (form fields, URL parameters)
  • Keys come from external sources (APIs, databases, CMS)
  • Keys come from URLs (route parameters, query strings)
  • You’re building a cache, lookup table, or dictionary
  • You don’t know in advance what keys will be used

Use regular {} when:

  • You control all the keys (they’re hardcoded in your code)
  • You’re creating a structured object with known properties
  • You need methods like .toString() or .hasOwnProperty()

8 Real-World Examples for Svelte 5 / SvelteKit

Now let’s see how this pattern applies specifically to Svelte 5 and SvelteKit applications. Each example shows a common scenario where Object.create(null) prevents potential bugs.

1. Safe Route Parameter Caching in SvelteKit

The Scenario: You’re building a blog with SvelteKit, and you want to cache database queries to improve performance. The cache keys are the URL slugs from your routes (like /blog/my-post).

The Problem: URL slugs come from users - someone could create a post with the slug “constructor” or ”proto”, which would conflict with inherited object properties.

The Solution:

// src/lib/server/cache.ts

/**
 * Creates a type-safe cache that's immune to prototype pollution.
 *
 * Why Object.create(null)?
 * When we cache blog posts by their URL slug, the slug comes from the URL.
 * A malicious user could request /blog/constructor or /blog/__proto__
 * and break our cache if we used a regular object.
 */
function createCache<T>() {
	// Object.create(null) ensures no inherited properties
	const store: Record<string, T> = Object.create(null)

	return {
		/**
		 * Get an item from the cache
		 * Returns undefined if the key doesn't exist (never returns inherited properties)
		 */
		get: (key: string): T | undefined => store[key],

		/**
		 * Store an item in the cache
		 */
		set: (key: string, value: T): void => {
			store[key] = value
		},

		/**
		 * Check if a key exists in the cache
		 * The `in` operator is safe here because there's no prototype chain
		 */
		has: (key: string): boolean => key in store,

		/**
		 * Remove an item from the cache
		 */
		delete: (key: string): void => {
			delete store[key]
		},

		/**
		 * Clear all items from the cache
		 */
		clear: (): void => {
			for (const key of Object.keys(store)) {
				delete store[key]
			}
		}
	}
}

// Create a cache for blog posts
export const postCache = createCache<{
	title: string
	content: string
	publishedAt: Date
}>()
// src/routes/blog/[slug]/+page.server.ts
import { postCache } from '$lib/server/cache'
import * as db from '$lib/server/database'
import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async ({ params }) => {
	// params.slug comes from the URL
	// Examples: "hello-world", "my-post", or even "constructor"!

	// Check if we have this post cached
	if (postCache.has(params.slug)) {
		console.log(`Cache hit for: ${params.slug}`)
		return { post: postCache.get(params.slug) }
	}

	// Not in cache, fetch from database
	console.log(`Cache miss for: ${params.slug}`)
	const post = await db.getPost(params.slug)

	// Store in cache for next time
	if (post) {
		postCache.set(params.slug, post)
	}

	return { post }
}

What could go wrong without Object.create(null)?

// If we used a regular object:
const unsafeCache = {}

// A request to /blog/constructor would NOT return undefined
console.log(unsafeCache['constructor']) // [Function: Object]

// Our cache.has() check would return true even for non-existent posts!
console.log('constructor' in unsafeCache) // true - WRONG!

2. Form Field Error Tracking with Svelte 5 Runes

The Scenario: You’re building a registration form with real-time validation. As users type, you want to show error messages next to each field.

The Problem: Form field names come from your HTML, and sometimes from dynamic sources like a form builder. If a field is named “constructor” or “toString”, using a regular object to track errors would cause problems.

The Solution:

<!-- src/routes/register/+page.svelte -->
<script lang="ts">
	import { enhance } from '$app/forms'
	import type { ActionData } from './$types'

	// Get form response from the server (if any)
	let { form }: { form: ActionData } = $props()

	/**
	 * Store validation errors for each field.
	 *
	 * Why Object.create(null)?
	 * Form field names could theoretically be anything, including
	 * "constructor", "toString", or "__proto__". Using Object.create(null)
	 * ensures that looking up any field name works correctly.
	 *
	 * The $state() rune makes this object reactive, so the UI updates
	 * automatically when errors change.
	 */
	let fieldErrors: Record<string, string> = $state(Object.create(null))

	/**
	 * Derived state that tells us if the form has any errors.
	 * This automatically updates whenever fieldErrors changes.
	 */
	let hasErrors = $derived(Object.keys(fieldErrors).length > 0)

	/**
	 * Count of total errors (useful for showing "3 errors remaining")
	 */
	let errorCount = $derived(Object.keys(fieldErrors).length)

	/**
	 * Validate a single field and update the errors object.
	 * @param name - The field name (could be any string)
	 * @param value - The current field value
	 */
	function validateField(name: string, value: string) {
		// Simple validation rules
		if (!value.trim()) {
			// Field is empty - add an error
			fieldErrors[name] = `${name} is required`
		} else if (name === 'email' && !value.includes('@')) {
			// Email validation
			fieldErrors[name] = 'Please enter a valid email address'
		} else if (name === 'password' && value.length < 8) {
			// Password length check
			fieldErrors[name] = 'Password must be at least 8 characters'
		} else {
			// Field is valid - remove any existing error
			delete fieldErrors[name]
		}
	}

	/**
	 * Clear all errors (useful for form reset)
	 */
	function clearErrors() {
		// Create a fresh empty object
		fieldErrors = Object.create(null)
	}
</script>

<h1>Create an Account</h1>

{#if errorCount > 0}
	<p class="error-summary">
		Please fix {errorCount} error{errorCount > 1 ? 's' : ''} below
	</p>
{/if}

<form method="POST" use:enhance>
	<!-- Username field -->
	<label>
		<span>Username</span>
		<input
			name="username"
			type="text"
			oninput={(e) => validateField('username', e.currentTarget.value)}
			class:invalid={fieldErrors['username']}
		/>
		{#if fieldErrors['username']}
			<span class="error">{fieldErrors['username']}</span>
		{/if}
	</label>

	<!-- Email field -->
	<label>
		<span>Email</span>
		<input
			name="email"
			type="email"
			oninput={(e) => validateField('email', e.currentTarget.value)}
			class:invalid={fieldErrors['email']}
		/>
		{#if fieldErrors['email']}
			<span class="error">{fieldErrors['email']}</span>
		{/if}
	</label>

	<!-- Password field -->
	<label>
		<span>Password</span>
		<input
			name="password"
			type="password"
			oninput={(e) => validateField('password', e.currentTarget.value)}
			class:invalid={fieldErrors['password']}
		/>
		{#if fieldErrors['password']}
			<span class="error">{fieldErrors['password']}</span>
		{/if}
	</label>

	<!-- 
    Example of a field with an unusual name.
    In a real app, this might come from a form builder or CMS.
  -->
	<label>
		<span>Company Constructor (legal entity name)</span>
		<input
			name="constructor"
			type="text"
			oninput={(e) => validateField('constructor', e.currentTarget.value)}
			class:invalid={fieldErrors['constructor']}
		/>
		{#if fieldErrors['constructor']}
			<span class="error">{fieldErrors['constructor']}</span>
		{/if}
	</label>

	<div class="actions">
		<button type="button" onclick={clearErrors}>Clear Errors</button>
		<button type="submit" disabled={hasErrors}>Create Account</button>
	</div>
</form>

<style>
	.error {
		color: red;
		font-size: 0.875rem;
	}

	.error-summary {
		background: #fee;
		border: 1px solid #fcc;
		padding: 1rem;
		border-radius: 4px;
	}

	.invalid {
		border-color: red;
	}
</style>

3. Dynamic Component Registry for Page Builders

The Scenario: You’re building a page builder or CMS where users can add different types of content blocks (text, images, videos, etc.). Component names come from the database or a configuration file.

The Problem: Component type names are strings that could be anything - including JavaScript reserved words or inherited property names.

The Solution:

// src/lib/components/registry.svelte.ts
import type { Component } from 'svelte'

/**
 * A registry for dynamically loading Svelte components by name.
 *
 * Why Object.create(null)?
 * Component names come from the CMS or database. A content editor might
 * name a component "Constructor" (for a page about construction) or
 * "Prototype" (for a prototyping tool section). With a regular object,
 * these names would clash with inherited properties.
 */
class ComponentRegistry {
	// Private storage with no prototype
	private components: Record<string, Component> = Object.create(null)

	/**
	 * Register a component with a given name.
	 * @param name - Unique identifier for the component (any string is safe)
	 * @param component - The Svelte component to register
	 */
	register(name: string, component: Component): void {
		// Check if already registered (safe because no prototype)
		if (name in this.components) {
			console.warn(`Component "${name}" is already registered. Skipping.`)
			return
		}

		this.components[name] = component
		console.log(`Registered component: ${name}`)
	}

	/**
	 * Register multiple components at once.
	 * @param components - Object mapping names to components
	 */
	registerMany(components: Record<string, Component>): void {
		for (const [name, component] of Object.entries(components)) {
			this.register(name, component)
		}
	}

	/**
	 * Get a component by name.
	 * @param name - The component name to look up
	 * @returns The component, or undefined if not found
	 */
	get(name: string): Component | undefined {
		return this.components[name]
	}

	/**
	 * Check if a component is registered.
	 * @param name - The component name to check
	 * @returns true if the component exists
	 */
	has(name: string): boolean {
		// The `in` operator is safe here because there's no prototype chain
		// With a regular object, `'constructor' in obj` would return true!
		return name in this.components
	}

	/**
	 * Get a list of all registered component names.
	 * @returns Array of component names
	 */
	list(): string[] {
		return Object.keys(this.components)
	}

	/**
	 * Remove a component from the registry.
	 * @param name - The component name to remove
	 */
	unregister(name: string): void {
		delete this.components[name]
	}
}

// Export a singleton instance
export const registry = new ComponentRegistry()
<!-- src/routes/page-builder/+page.svelte -->
<script lang="ts">
	import { registry } from '$lib/components/registry.svelte'

	// Import your block components
	import TextBlock from '$lib/components/blocks/TextBlock.svelte'
	import ImageBlock from '$lib/components/blocks/ImageBlock.svelte'
	import VideoBlock from '$lib/components/blocks/VideoBlock.svelte'
	import ConstructorBlock from '$lib/components/blocks/ConstructorBlock.svelte'

	// Register all available components
	registry.registerMany({
		text: TextBlock,
		image: ImageBlock,
		video: VideoBlock,
		constructor: ConstructorBlock, // A block about construction - works fine!
		prototype: TextBlock // A block about prototyping - also safe!
	})

	// Page data from the server (loaded from CMS)
	let { data } = $props()

	// data.blocks might look like:
	// [
	//   { type: 'text', props: { content: 'Hello world' } },
	//   { type: 'image', props: { src: '/photo.jpg', alt: 'A photo' } },
	//   { type: 'constructor', props: { content: 'Building tips...' } },
	// ]
</script>

<main class="page-builder">
	<h1>{data.pageTitle}</h1>

	{#each data.blocks as block, index}
		{#if registry.has(block.type)}
			<!-- Dynamically render the correct component -->
			{@const BlockComponent = registry.get(block.type)}
			<section class="block" data-type={block.type}>
				<BlockComponent {...block.props} />
			</section>
		{:else}
			<!-- Handle unknown component types gracefully -->
			<section class="block block--unknown">
				<p>⚠️ Unknown block type: "{block.type}"</p>
				<p>Available types: {registry.list().join(', ')}</p>
			</section>
		{/if}
	{/each}
</main>

<style>
	.block {
		padding: 1rem;
		margin: 1rem 0;
		border: 1px solid #eee;
		border-radius: 8px;
	}

	.block--unknown {
		background: #fff3cd;
		border-color: #ffc107;
	}
</style>

4. Server-Side Permission Checking in Hooks

The Scenario: You’re implementing role-based access control (RBAC) in your SvelteKit app. Different routes require different permissions, and you want to check permissions in a server hook before the page loads.

The Problem: Route paths are strings, and some valid routes might have names that conflict with object properties (like /api/constructor for a construction API).

The Solution:

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit'

/**
 * Maps route paths to required permissions.
 *
 * Why Object.create(null)?
 * Route paths are strings that we use as object keys. While most paths
 * are normal (like '/admin' or '/dashboard'), you might have routes like:
 * - /api/constructor (for a construction company API)
 * - /docs/prototype (for prototyping documentation)
 * - /legal/__proto__ (unlikely, but possible in user-generated routes)
 *
 * Using Object.create(null) ensures all paths work correctly.
 */
const routePermissions: Record<string, string[]> = Object.create(null)

// Define which routes require which permissions
// Public routes (no permissions needed) are simply not listed here
routePermissions['/admin'] = ['admin']
routePermissions['/admin/users'] = ['admin', 'user-manager']
routePermissions['/admin/settings'] = ['admin']
routePermissions['/dashboard'] = ['user', 'admin']
routePermissions['/dashboard/analytics'] = ['analyst', 'admin']
routePermissions['/api/constructor'] = ['developer'] // Works correctly!
routePermissions['/api/prototype'] = ['developer'] // Also works!

/**
 * Check if a user has at least one of the required permissions.
 * @param userPermissions - Array of permissions the user has
 * @param requiredPermissions - Array of permissions needed for the route
 * @returns true if the user has at least one required permission
 */
function hasPermission(userPermissions: string[], requiredPermissions: string[]): boolean {
	return requiredPermissions.some((permission) => userPermissions.includes(permission))
}

/**
 * The main request handler hook.
 * This runs for every request before the route is resolved.
 */
export const handle: Handle = async ({ event, resolve }) => {
	const { pathname } = event.url

	// Look up permissions for this route
	// With Object.create(null), this is safe for ANY pathname
	const requiredPermissions = routePermissions[pathname]

	// If no permissions required, allow access
	if (!requiredPermissions) {
		return resolve(event)
	}

	// Get the current user from locals (set by auth middleware)
	const user = event.locals.user

	// No user? Redirect to login
	if (!user) {
		return new Response(null, {
			status: 302,
			headers: { Location: `/login?redirectTo=${encodeURIComponent(pathname)}` }
		})
	}

	// Check if user has required permissions
	if (!hasPermission(user.permissions, requiredPermissions)) {
		// User doesn't have permission
		return new Response(
			JSON.stringify({
				error: 'Forbidden',
				message: 'You do not have permission to access this resource',
				required: requiredPermissions
			}),
			{
				status: 403,
				headers: { 'Content-Type': 'application/json' }
			}
		)
	}

	// User has permission, continue to the route
	return resolve(event)
}

5. Reactive Feature Flags with createContext

The Scenario: You’re implementing feature flags to gradually roll out new features. Flag names come from a feature flag service or configuration, and you want them to be reactive so the UI updates when flags change.

The Problem: Feature flag names are arbitrary strings defined by your team or a third-party service. Names like “constructor-redesign” or “prototype-mode” could cause issues with regular objects.

The Solution:

// src/lib/features/context.svelte.ts
import { createContext } from 'svelte'

/**
 * Type definition for our feature flags context.
 */
interface FeatureFlags {
	/** Check if a specific flag is enabled */
	isEnabled: (flag: string) => boolean
	/** Enable or disable a flag at runtime */
	setFlag: (flag: string, enabled: boolean) => void
	/** Get all flag names */
	listFlags: () => string[]
}

/**
 * Create a type-safe context for feature flags.
 *
 * createContext() returns a [get, set] tuple:
 * - get() retrieves the context value (throws if not set)
 * - set() sets the context value
 *
 * This is available in Svelte 5.40.0 and later.
 */
const [getFeatureFlags, setFeatureFlags] = createContext<FeatureFlags>()

// Export the getter for use in child components
export { getFeatureFlags }

/**
 * Initialize the feature flags context.
 * Call this in your root layout to make flags available throughout the app.
 *
 * @param initialFlags - Initial flag values from server/config
 * @returns The feature flags context
 *
 * Why Object.create(null)?
 * Feature flag names come from configuration or external services.
 * Real examples that could cause issues with regular objects:
 * - "constructor-v2" (new constructor pattern)
 * - "prototype-testing" (A/B test for prototype feature)
 * - "toString-format" (different string formatting)
 *
 * Using Object.create(null) ensures ALL flag names work correctly.
 */
export function initFeatureFlags(initialFlags: Record<string, boolean> = {}) {
	// Create reactive state with a null prototype
	// This combines Svelte 5's reactivity with prototype safety
	let flags: Record<string, boolean> = $state(Object.create(null))

	// Safely copy initial flags
	// We iterate and assign instead of using Object.assign to ensure
	// we don't accidentally copy prototype properties
	for (const [flagName, isEnabled] of Object.entries(initialFlags)) {
		flags[flagName] = isEnabled
	}

	// Create the context object
	const context: FeatureFlags = {
		/**
		 * Check if a feature flag is enabled.
		 * Returns false for unknown flags (fail-safe default).
		 */
		isEnabled: (flag: string): boolean => {
			// Safe lookup - works for ANY flag name
			return flags[flag] === true
		},

		/**
		 * Enable or disable a flag at runtime.
		 * Useful for testing or gradual rollouts.
		 */
		setFlag: (flag: string, enabled: boolean): void => {
			flags[flag] = enabled
			// Reactivity automatically updates all components using this flag!
		},

		/**
		 * Get a list of all defined flags.
		 */
		listFlags: (): string[] => {
			return Object.keys(flags)
		}
	}

	// Set the context and return it
	return setFeatureFlags(context)
}
<!-- src/routes/+layout.svelte -->
<script lang="ts">
	import { initFeatureFlags } from '$lib/features/context.svelte'

	let { data, children } = $props()

	// Initialize feature flags from server data
	// These might come from a feature flag service like LaunchDarkly, Split, etc.
	initFeatureFlags(data.featureFlags)

	// data.featureFlags might look like:
	// {
	//   'new-dashboard': true,
	//   'dark-mode': false,
	//   'constructor-pattern': true,  // Safe!
	//   'prototype-mode': false,       // Also safe!
	// }
</script>

<!-- Render the rest of the app -->
{@render children()}
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
	import { getFeatureFlags } from '$lib/features/context.svelte'

	// Get the feature flags context
	// This will throw an error if initFeatureFlags wasn't called in a parent,
	// helping you catch configuration mistakes early
	const features = getFeatureFlags()
</script>

<h1>Dashboard</h1>

<!-- Conditionally render based on feature flags -->
{#if features.isEnabled('new-dashboard')}
	<p>🎉 You're seeing the new dashboard design!</p>
	<NewDashboard />
{:else}
	<LegacyDashboard />
{/if}

<!-- Even flags with "dangerous" names work correctly -->
{#if features.isEnabled('constructor-pattern')}
	<section class="constructor-features">
		<h2>Constructor Pattern Tools</h2>
		<!-- ... -->
	</section>
{/if}

<!-- Toggle flags for testing -->
<div class="debug-panel">
	<h3>Feature Flags (Debug)</h3>
	<ul>
		{#each features.listFlags() as flag}
			<li>
				<label>
					<input
						type="checkbox"
						checked={features.isEnabled(flag)}
						onchange={() => features.setFlag(flag, !features.isEnabled(flag))}
					/>
					{flag}
				</label>
			</li>
		{/each}
	</ul>
</div>

6. i18n Translation Keys

The Scenario: You’re building a multi-language website. Translation keys are defined in JSON files or loaded from a translation management system.

The Problem: Translation keys are arbitrary strings like “button.submit” or “error.constructor” (for error messages about constructors). These strings are used as object keys to look up translations.

The Solution:

// src/lib/i18n/translations.svelte.ts
import { createContext } from 'svelte'

/**
 * Type for our translations object.
 * Keys are translation keys, values are the translated strings.
 */
type Translations = Record<string, string>

/**
 * Type for our translation store context.
 */
type TranslationStore = {
	/** Translate a key, with optional fallback */
	t: (key: string, fallback?: string) => string
	/** Current locale code (e.g., 'en', 'de', 'fr') */
	locale: string
	/** Change the current locale */
	setLocale: (locale: string) => Promise<void>
}

// Create type-safe context
const [getI18n, setI18n] = createContext<TranslationStore>()

export { getI18n }

/**
 * Initialize the internationalization system.
 *
 * @param initialLocale - The starting locale (e.g., 'en')
 * @param initialTranslations - Translation key-value pairs
 *
 * Why Object.create(null)?
 * Translation keys come from JSON files created by translators.
 * Real examples that would break with regular objects:
 * - "constructor.title" → "Constructor Pattern"
 * - "error.hasOwnProperty" → "Property Error"
 * - "prototype.description" → "About Prototypes"
 *
 * With Object.create(null), ANY translation key works safely.
 */
export function initI18n(initialLocale: string, initialTranslations: Translations) {
	// Create reactive state for translations (null prototype = safe)
	let translations: Translations = $state(Object.create(null))

	// Reactive state for current locale
	let locale = $state(initialLocale)

	// Safely copy initial translations
	for (const [key, value] of Object.entries(initialTranslations)) {
		translations[key] = value
	}

	const store: TranslationStore = {
		/**
		 * Get a translation by key.
		 *
		 * @param key - The translation key (e.g., "button.submit")
		 * @param fallback - Optional fallback if key not found
		 * @returns The translation, fallback, or the key itself
		 *
		 * The lookup is safe for ANY key because we use Object.create(null)
		 */
		t: (key: string, fallback?: string): string => {
			// Look up the translation
			const translation = translations[key]

			// Return translation if found
			if (translation !== undefined) {
				return translation
			}

			// Return fallback if provided
			if (fallback !== undefined) {
				return fallback
			}

			// Return the key itself (helps identify missing translations)
			console.warn(`Missing translation: ${key}`)
			return key
		},

		/** Get the current locale */
		get locale() {
			return locale
		},

		/**
		 * Change the locale and load new translations.
		 */
		setLocale: async (newLocale: string): Promise<void> => {
			// Don't reload if same locale
			if (newLocale === locale) return

			try {
				// Fetch translations for the new locale
				const response = await fetch(`/api/translations/${newLocale}`)

				if (!response.ok) {
					throw new Error(`Failed to load translations for ${newLocale}`)
				}

				const newTranslations = await response.json()

				// Reset translations with a fresh null-prototype object
				translations = Object.create(null)

				// Copy new translations safely
				for (const [key, value] of Object.entries(newTranslations)) {
					translations[key] = value as string
				}

				// Update locale
				locale = newLocale

				console.log(`Locale changed to: ${newLocale}`)
			} catch (error) {
				console.error(`Failed to change locale to ${newLocale}:`, error)
				throw error
			}
		}
	}

	return setI18n(store)
}
<!-- src/routes/+page.svelte -->
<script lang="ts">
	import { getI18n } from '$lib/i18n/translations.svelte'

	const { t, locale, setLocale } = getI18n()

	// Available languages
	const languages = [
		{ code: 'en', name: 'English' },
		{ code: 'de', name: 'Deutsch' },
		{ code: 'fr', name: 'Français' }
	]
</script>

<header>
	<!-- Language selector -->
	<select value={locale} onchange={(e) => setLocale(e.currentTarget.value)}>
		{#each languages as lang}
			<option value={lang.code}>{lang.name}</option>
		{/each}
	</select>
</header>

<main>
	<!-- Normal translations -->
	<h1>{t('welcome.title', 'Welcome!')}</h1>
	<p>{t('welcome.description', 'Thanks for visiting our site.')}</p>

	<!-- 
    Translation keys that would break with regular objects work fine!
    Imagine your translations JSON has:
    {
      "constructor.title": "Constructor Pattern",
      "constructor.description": "Learn about the constructor pattern",
      "prototype.learn": "Understanding Prototypes"
    }
  -->
	<section>
		<h2>{t('constructor.title')}</h2>
		<p>{t('constructor.description')}</p>
	</section>

	<button>{t('button.submit', 'Submit')}</button>
</main>

7. URL Query Parameter State Management

The Scenario: You’re building a product listing page with filters for category, price range, sorting, etc. You want the filter state to be stored in the URL so users can share filtered views.

The Problem: URL query parameters are completely user-controlled. Someone could manually add ?constructor=value or ?__proto__=hack to the URL.

The Solution:

<!-- src/lib/components/FilterPanel.svelte -->
<script lang="ts">
	import { page } from '$app/state'
	import { goto } from '$app/navigation'

	/**
	 * Parse URL search parameters into a safe object.
	 *
	 * Why Object.create(null)?
	 * URL parameters come directly from the browser's address bar.
	 * Users can type ANYTHING into the URL, including:
	 * - ?constructor=value
	 * - ?__proto__=malicious
	 * - ?hasOwnProperty=oops
	 *
	 * Using Object.create(null) ensures we handle ALL parameter names safely.
	 *
	 * @returns A safe object containing all URL parameters
	 */
	function parseFilters(): Record<string, string> {
		// Create a truly empty object
		const filters: Record<string, string> = Object.create(null)

		// Iterate through all URL parameters
		// page.url.searchParams is a URLSearchParams object
		for (const [key, value] of page.url.searchParams) {
			filters[key] = value
		}

		return filters
	}

	/**
	 * Reactive derived state that automatically updates when the URL changes.
	 * Svelte's $derived tracks page.url.searchParams and re-runs parseFilters()
	 * whenever the URL changes.
	 */
	let filters = $derived(parseFilters())

	/**
	 * Computed property: check if any filters are active
	 */
	let hasActiveFilters = $derived(Object.keys(filters).length > 0)

	/**
	 * Computed property: count of active filters
	 */
	let filterCount = $derived(Object.keys(filters).length)

	/**
	 * Update a single filter in the URL.
	 *
	 * @param name - The parameter name
	 * @param value - The new value (empty string to remove)
	 */
	async function setFilter(name: string, value: string) {
		// Create a copy of the current URL
		const url = new URL(page.url)

		if (value) {
			// Set the parameter
			url.searchParams.set(name, value)
		} else {
			// Remove the parameter if value is empty
			url.searchParams.delete(name)
		}

		// Navigate to the new URL without full page reload
		await goto(url, {
			replaceState: true, // Don't add to browser history
			noScroll: true // Don't scroll to top
		})
	}

	/**
	 * Clear all filters from the URL.
	 */
	async function clearFilters() {
		await goto(page.url.pathname, {
			replaceState: true
		})
	}

	/**
	 * Remove a specific filter.
	 */
	async function removeFilter(name: string) {
		await setFilter(name, '')
	}
</script>

<div class="filter-panel">
	<h3>
		Filters
		{#if filterCount > 0}
			<span class="badge">{filterCount}</span>
		{/if}
	</h3>

	<!-- Search input -->
	<div class="filter-group">
		<label for="search">Search</label>
		<input
			id="search"
			type="text"
			placeholder="Search products..."
			value={filters['q'] ?? ''}
			oninput={(e) => setFilter('q', e.currentTarget.value)}
		/>
	</div>

	<!-- Category filter -->
	<div class="filter-group">
		<label for="category">Category</label>
		<select
			id="category"
			value={filters['category'] ?? ''}
			onchange={(e) => setFilter('category', e.currentTarget.value)}
		>
			<option value="">All Categories</option>
			<option value="electronics">Electronics</option>
			<option value="clothing">Clothing</option>
			<option value="books">Books</option>
			<option value="constructor">Construction Tools</option>
			<!-- Safe! -->
		</select>
	</div>

	<!-- Sort order -->
	<div class="filter-group">
		<label for="sort">Sort By</label>
		<select
			id="sort"
			value={filters['sort'] ?? 'date'}
			onchange={(e) => setFilter('sort', e.currentTarget.value)}
		>
			<option value="date">Newest First</option>
			<option value="price-asc">Price: Low to High</option>
			<option value="price-desc">Price: High to Low</option>
			<option value="name">Name</option>
		</select>
	</div>

	<!-- Active filters display -->
	{#if hasActiveFilters}
		<div class="active-filters">
			<h4>Active Filters:</h4>
			<div class="filter-tags">
				{#each Object.entries(filters) as [key, value]}
					<span class="filter-tag">
						<strong>{key}:</strong>
						{value}
						<button onclick={() => removeFilter(key)} aria-label={`Remove ${key} filter`}>
							×
						</button>
					</span>
				{/each}
			</div>
			<button class="clear-all" onclick={clearFilters}> Clear All Filters </button>
		</div>
	{/if}
</div>

<style>
	.filter-panel {
		padding: 1rem;
		background: #f5f5f5;
		border-radius: 8px;
	}

	.filter-group {
		margin-bottom: 1rem;
	}

	.filter-group label {
		display: block;
		margin-bottom: 0.25rem;
		font-weight: 500;
	}

	.badge {
		background: #007bff;
		color: white;
		padding: 0.125rem 0.5rem;
		border-radius: 999px;
		font-size: 0.75rem;
		margin-left: 0.5rem;
	}

	.active-filters {
		margin-top: 1rem;
		padding-top: 1rem;
		border-top: 1px solid #ddd;
	}

	.filter-tags {
		display: flex;
		flex-wrap: wrap;
		gap: 0.5rem;
		margin-bottom: 0.5rem;
	}

	.filter-tag {
		background: white;
		border: 1px solid #ddd;
		padding: 0.25rem 0.5rem;
		border-radius: 4px;
		font-size: 0.875rem;
	}

	.filter-tag button {
		background: none;
		border: none;
		cursor: pointer;
		margin-left: 0.25rem;
		color: #666;
	}

	.clear-all {
		font-size: 0.875rem;
		color: #666;
		background: none;
		border: none;
		cursor: pointer;
		text-decoration: underline;
	}
</style>

8. Event Handler Registry for Custom Actions

The Scenario: You’re building a reusable Svelte action that can attach multiple event listeners to an element. The action accepts an object where keys are event names and values are handler functions.

The Problem: Event names are strings provided by the consumer of your action. While most are standard DOM events, custom events could have any name.

The Solution:

// src/lib/actions/multiEvent.ts
import type { Action } from 'svelte/action'

/**
 * Type for event handlers object.
 * Keys are event names, values are handler functions.
 */
type EventHandlers = Record<string, (event: Event) => void>

/**
 * A Svelte action that attaches multiple event listeners to an element.
 *
 * Usage:
 * <div use:multiEvent={{ click: handleClick, mouseenter: handleHover }}>
 *
 * Why Object.create(null)?
 * Event names are provided by the user of this action. While most are
 * standard DOM events (click, mouseenter, etc.), the action should handle:
 * - Custom events: 'my-custom-event', 'data:loaded'
 * - Edge cases: 'constructor', 'toString' (unlikely but possible)
 *
 * Using Object.create(null) for internal storage ensures we correctly
 * track cleanup functions for ANY event name.
 *
 * @param node - The DOM element to attach listeners to
 * @param handlers - Object mapping event names to handler functions
 */
export const multiEvent: Action<HTMLElement, EventHandlers> = (node, handlers = {}) => {
	/**
	 * Store cleanup functions for each event.
	 * Using Object.create(null) ensures we can use any event name as a key.
	 */
	const cleanups: Record<string, () => void> = Object.create(null)

	/**
	 * Attach event handlers and store cleanup functions.
	 * @param eventHandlers - Object mapping event names to handlers
	 */
	function attach(eventHandlers: EventHandlers): void {
		// First, clean up any existing handlers
		for (const eventName of Object.keys(cleanups)) {
			cleanups[eventName]()
			delete cleanups[eventName]
		}

		// Attach new handlers
		for (const [eventName, handler] of Object.entries(eventHandlers)) {
			// Skip invalid handlers
			if (typeof handler !== 'function') {
				console.warn(`Invalid handler for event "${eventName}": expected function`)
				continue
			}

			// Add the event listener
			node.addEventListener(eventName, handler)

			// Store cleanup function
			cleanups[eventName] = () => {
				node.removeEventListener(eventName, handler)
			}
		}
	}

	// Initial attachment
	attach(handlers)

	// Return action lifecycle methods
	return {
		/**
		 * Called when the handlers parameter changes.
		 * Re-attaches all event listeners.
		 */
		update(newHandlers: EventHandlers): void {
			attach(newHandlers)
		},

		/**
		 * Called when the element is removed from the DOM.
		 * Cleans up all event listeners.
		 */
		destroy(): void {
			for (const cleanup of Object.values(cleanups)) {
				cleanup()
			}
		}
	}
}

/**
 * Convenience type for better autocompletion.
 * Extend this if you want to add specific event types.
 */
export type StandardEventHandlers = {
	click?: (event: MouseEvent) => void
	mouseenter?: (event: MouseEvent) => void
	mouseleave?: (event: MouseEvent) => void
	focus?: (event: FocusEvent) => void
	blur?: (event: FocusEvent) => void
	keydown?: (event: KeyboardEvent) => void
	keyup?: (event: KeyboardEvent) => void
	[customEvent: string]: ((event: Event) => void) | undefined
}
<!-- Example usage of the multiEvent action -->
<script lang="ts">
	import { multiEvent } from '$lib/actions/multiEvent'

	// Track interaction state
	let clickCount = $state(0)
	let isHovered = $state(false)
	let lastEvent = $state('')

	// Reference to the element (for dispatching custom events)
	let boxElement: HTMLElement

	// Define event handlers
	// These can include any event names, including unusual ones
	let eventConfig = $state({
		// Standard DOM events
		click: () => {
			clickCount++
			lastEvent = 'click'
		},
		mouseenter: () => {
			isHovered = true
			lastEvent = 'mouseenter'
		},
		mouseleave: () => {
			isHovered = false
			lastEvent = 'mouseleave'
		},

		// Custom events (might be dispatched by other components)
		'data:loaded': () => {
			lastEvent = 'data:loaded'
			console.log('Data loaded event received!')
		},

		// Even event names that match object properties work!
		'custom:constructor': () => {
			lastEvent = 'custom:constructor'
			console.log('Constructor event received!')
		}
	})

	/**
	 * Dispatch a custom event for testing.
	 */
	function dispatchCustomEvent(eventName: string) {
		if (boxElement) {
			const event = new CustomEvent(eventName, { bubbles: true })
			boxElement.dispatchEvent(event)
		}
	}
</script>

<div
	class="interactive-box"
	class:hovered={isHovered}
	use:multiEvent={eventConfig}
	bind:this={boxElement}
>
	<p>Click me! Count: {clickCount}</p>
	<p>Hovered: {isHovered ? 'Yes' : 'No'}</p>
	<p>Last event: {lastEvent || 'None yet'}</p>
</div>

<!-- Buttons to trigger custom events for testing -->
<div class="test-buttons">
	<button onclick={() => dispatchCustomEvent('data:loaded')}> Trigger data:loaded </button>
	<button onclick={() => dispatchCustomEvent('custom:constructor')}>
		Trigger custom:constructor
	</button>
</div>

<style>
	.interactive-box {
		padding: 2rem;
		background: #f0f0f0;
		border: 2px solid #ccc;
		border-radius: 8px;
		cursor: pointer;
		transition: all 0.2s;
	}

	.interactive-box.hovered {
		background: #e0e0ff;
		border-color: #66f;
	}

	.test-buttons {
		margin-top: 1rem;
		display: flex;
		gap: 0.5rem;
	}
</style>

Quick Reference: When to Use What

SituationUse {}Use Object.create(null)
Keys are hardcoded in your codeSafeNot needed
Keys come from user inputRiskySafe
Keys come from URLsRiskySafe
Keys come from APIs or databasesRiskySafe
You need .toString() or other methodsSafeWon’t have them
Building a cache or lookup tableRiskySafe
Using with Svelte 5 $stateWorksWorks

Common Questions

Can I use Map instead?

Yes! JavaScript’s Map is another safe option for key-value storage:

const safeMap = new Map()
safeMap.set('constructor', 'my value')
console.log(safeMap.get('constructor')) // 'my value' ✅

Svelte even provides a reactive SvelteMap in svelte/reactivity. However, Object.create(null) has some advantages:

  • Familiar object syntax (obj.key or obj['key'])
  • Works directly with $state for reactivity
  • JSON-serializable (important for SSR in SvelteKit)
  • Often more convenient for simple lookup tables

Does this work with TypeScript?

Absolutely! Use Record<string, T> for type safety:

const cache: Record<string, User> = Object.create(null)
cache['alice'] = { name: 'Alice', age: 30 } // ✅ Type-checked

What about performance?

For most applications, the performance difference is negligible. Object.create(null) can actually be slightly faster for property lookups because JavaScript doesn’t need to check the prototype chain. But unless you’re doing millions of lookups, you won’t notice a difference.


Summary

Object.create(null) is a simple but powerful technique that every Svelte 5 and SvelteKit developer should know. Here’s when to reach for it:

  1. Always use it when keys come from untrusted sources (URLs, user input, APIs)
  2. Use it for caches, lookup tables, registries, and dictionaries
  3. Skip it when you control all keys and need inherited methods

In SvelteKit applications where data flows from URLs, forms, databases, and APIs, this pattern helps you avoid an entire category of subtle bugs. Combined with Svelte 5’s $state rune, you get reactive, type-safe dictionaries that handle any key safely.

The next time you write const cache = {}, ask yourself: “Where do these keys come from?” If the answer is “from the user” or “from external data,” make it Object.create(null) instead. Your future self will thank you!