I often try to make everything as dynamic as possible, until TypeScript reminds me that’s not always a good idea.
One of the best things about TypeScript is that it pushes you to make things known at compile time, not at runtime. In practice, that means 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 (or a unique symbol).
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.
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
Or you can use a unique symbol:
const MyIdentifier = Symbol("id"); // This is a unique symbol
type Data = {
[MyIdentifier]: string; // ✅ Works!
};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".
const Plugins = {
Auth: {
internalKey: "user_token",
version: 1,
},
Analytics: {
internalKey: "session_id",
version: 2,
},
} as const; // <--- The magic wand
// Now this is perfectly valid!
type PluginState = {
[Plugins.Auth.internalKey]: string;
};
// Resulting type is effectively: { "user_token": string }In the end, I wanted to put configurations for multiple items in one config object, then derive a centralized type from the id fields.
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;
}
*/The main takeaway for me: when you want TypeScript to “lift” values into types (especially for computed keys), you need to keep those values literal—const + as const is often the difference.