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).
let binding widens to stringDeclaring 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 literalTo 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 wideningI 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 typesas 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 }keyof typeofIn 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.
Occasional notes on software, tools, and things I learn. No spam.
Unsubscribe anytime.