omiid
homenotebookai usage

The trap of making everything dynamic

March 01, 2024 · Updated on August 09, 2026

Making every value dynamic looks like the flexible choice. TypeScript works the other way: it pushes you to make things known at compile time, not at runtime. In practice, types work best when they can be derived from static, fixed values.

A common place this shows up is computed property names in types and interfaces: the key has to be something TypeScript can treat as a literal type (or a unique symbol).

I write short, practical notes like this one. Get the next one by email:

Unsubscribe anytime.

Computed keys must be literals

A let binding widens to string

Declaring the key with let is enough to break the type:

let myKey = "id"; // TypeScript sees this as type 'string', not specifically "id"
 
type User = {
	[myKey]: number; // ❌ Error: myKey is a general string, not a literal.
};

Because myKey is let, its type is widened to string. TypeScript can’t use a general string as a computed key in a type literal.

const and unique symbol keep the key literal

To fix it, you need myKey to be a value that TypeScript can treat as fixed.

You can make it a const:

const myKey = "id"; // Because it's a const, the type is "id", not 'string'
 
type User = {
	[myKey]: number; // ✅ Works!
};

Or you can use a unique symbol:

const MyIdentifier = Symbol("id"); // This is a unique symbol
 
type Data = {
	[MyIdentifier]: string; // ✅ Works!
};

const alone does not stop nested values from widening

I was adding multiple plugins to a system, and each plugin had its own unique identifier key. I started with a structure like this:

const Plugins = {
	Auth: {
		internalKey: "user_token",
		version: 1,
	},
	Analytics: {
		internalKey: "session_id",
		version: 2,
	},
};

The goal was to create a type that uses each plugin’s internalKey as the property name.

Something like this:

type PluginState = {
	[Plugins.Auth.internalKey]: string;
};

That’s when TypeScript gave me this error:

A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.

Even though Plugins is a const, without as const the nested internalKey values get widened to string. From the type system’s perspective, Plugins.Auth.internalKey is just string, not the literal type "user_token".

as const pins the nested values to literal types

as const is a const assertion. It tells TypeScript to infer the narrowest literal type for every value in the object, nested ones included:

const Plugins = {
	Auth: {
		internalKey: "user_token",
		version: 1,
	},
	Analytics: {
		internalKey: "session_id",
		version: 2,
	},
} as const; // <--- this keeps every nested string literal
 
// Now this is perfectly valid!
type PluginState = {
	[Plugins.Auth.internalKey]: string;
};
 
// Resulting type is effectively: { "user_token": string }

Derive the key union from the config object with keyof typeof

In the end, I wanted to put configurations for multiple items in one config object, then derive a centralized type from the id fields. keyof typeof reads the object’s own keys, and a mapped type turns the resulting union into an object type.

const AppConfig = {
	featureA: { id: "feat_a", active: true },
	featureB: { id: "feat_b", active: false },
} as const;
 
// Create a union of all 'id' values: "feat_a" | "feat_b"
type AllFeatureIds = (typeof AppConfig)[keyof typeof AppConfig]["id"];
 
// Use that union to drive an object type
type FeaturePermissions = {
	[K in AllFeatureIds]: boolean;
};
 
/*
Resulting Type:
type FeaturePermissions = {
    feat_a: boolean;
    feat_b: boolean;
}
*/

When you want TypeScript to lift values into types, especially for computed keys, you have to keep those values literal. const plus as const is usually the whole fix.

Join My Newsletter

Occasional notes on software, tools, and things I learn. No spam.

Unsubscribe anytime.

Continue Reading
  • Tuning Postgres and pgvector: the three knobs that matter08-18-2026 · Most pgvector performance problems come down to three settings. This post shows how to read an ANN query plan and tune ef_search, shared_buffers, and work_mem in the right order.
  • AI text watermarking: how it works and what it can't do08-16-2026 · Claude now watermarks its text. The watermark changes where the randomness in word choice comes from, not what the model can say. Here is the whole pipeline, with simulations you can poke at.
  • HNSW vs IVFFlat: choosing and building your pgvector index08-14-2026 · Past a few hundred thousand rows, an exact scan stops being fast enough. Here is how to pick between HNSW and IVFFlat and build the index without locking the table.
  • Vector search relevance: chunking, metadata, and the 0.81 problem08-11-2026 · Most bad vector search results come from one of three failure modes: chunking, modality mismatch, or a confused model. Each one has a specific diagnostic and a specific fix.
  • pgvector setup: your first multimodal query in TypeScript08-03-2026 · One Postgres table can hold text and screenshot embeddings in the same vector column. This post sets up the schema, the Voyage embedding call, and the first query that returns both.