Presumably your issue is that new Foo() produces an instance of Foo<string>, and string absorbs any string literal types when joined via a union. That is, string | "a" | "b" is just string, which completely forgets all about "a" and "b".
All that needs to be done here is to pick a default value for K such that it represents the absence of any strings. Some type which extends string but which has no values, so that when you join "a" to it in a union, you get "a" out. Luckily that type exists, and it's called never. As a bottom type, never extends every type including string, and is absorbed by every other type when you join to it with a union. So never | "a" | "b" will be "a" | "b".
Here goes:
export class Foo<K extends string = never> { // K has a default now
addKey<L extends string>(key: L): Foo<K | L> {
return this;
}
getKey(key: K) {
}
}
And let's test it:
const x = new Foo().addKey('a').addKey('b');
x.getKey('') // error!
// ----> ~~
// Argument of type '""' is not assignable to parameter of type '"a" | "b"'.
Looks good to me. Hope that helps; good luck!
Link to code
|) and not an intersection (&).