In order for the compiler to keep track of the literal type of the key passed in, we need MyClass to be generic in that type. So instead of MyClass, you'll have MyClass<K extends string>.
class MyClass<K extends string> {
Then, you want to say that data has a key of type K whose property value at that key is string. This is potentially confusing, because the syntax for a computed property name (e.g., {[keyExpression]: ValType}), the syntax for an index signature (e.g., {[dummyKeyName: KeyType]: ValType}), and the syntax for a mapped type (e.g., {[P in KeyType]: ValType }) look so similar, yet have different rules for what you can and can't do.
You can't write { [this.key]: string } because that would be a computed property key, which, as the error says, needs to have a statically known literal/symbol type. Instead, since the key type K is generic, you need to use a mapped type. So you could write {[P in K]: string}, or the equivalent Record<K, string> using the Record<K, V> utility type.
Oh, and if data should also have other properties, you will need to intersect Record<K, string> with the rest of the object type, because mapped types don't allow you to add other entries.
data: Record<K, string> & { otherProps: { [k: string]: number | undefined }
And then you need the constructor. You could have it take a key of type K, but there's the chance that someone could pass in "otherProps", which would collide with the existing otherProps property. To prevent that, we can write Exclude<K, "otherProps"> using the Exclude<T, U> utility type. If the type of key does not include "otherProps", then Exclude<K, "otherProps"> is just K. If it does, then Exclude<K, "otherProps"> won't be assignable to K and the compiler will be unhappy about calling that constructor:
constructor(key: Exclude<K, "otherProps">) {
this.data = { [key]: "prop", otherProps: { a: 1, b: 2, c: 3 } } as typeof this.data;
}
}
new MyClass("okay"); // okay
new MyClass("otherProps"); // error!
And now let's test it out:
const c = new MyClass("hello"); // okay
console.log(c.data.hello.toUpperCase()); // PROP
console.log(Object.entries(c.data.otherProps).map(
([k, v]) => k + ":" + v?.toFixed(2)).join("; ")) // "a:1.00; b:2.00; c:3.00"
Looks good. The compiler is aware that c.data has a string-valued hello property, while still accepting the otherProps property as a dictionary of numbers.
Playground link to code