@jscalz 's comment made me reconsider my answer.
From a classical OOP perspective, the extends constraint in bar<T extends Foo>(arg: T) on the generic parameter would mean that T possesses all properties of Foo and more. So with this constraint you can safely assume in your bar that arg behaves at least like Foo (and has the same property structure).
However, in your case, T is neither a class or an interface but a union of string literals and this is where the extends gets pointless:
Let's assume bar<T extends Foo>(arg: T) worked as you want it to (as stated in your OP), meaning, T is a superset of string literals on top of Foo. This would mean that in your bar() function you would have no control at all over which values arg holds – it may hold any kind of values and you might as well use any.
Given the above, your only chance to control what makes it's way into bar() is to use function bar(args: Bar), where Bar = Foo | 'c' which then also allows you to call bar('c'); without casting.
Original answer:
Defining type Bar as intersection type seems to at least tame the typescript linting errors: type Bar = Foo & 'c';.
Below I list the relevant parts (at least to my understanding) from the typescript spec that define that an intersection of types actually leads to a "Subtype-Supertype" relationship which is recognized by the extends constraint:
In 3.11.3 Subtypes and Supertypes it says:
S is a subtype of a type T, and T is a supertype of S, if S has no excess properties with respect to T (3.11.5) and one of the following is true:
[...]
- S is a union type and each constituent type of S is a subtype of T.
Moreover in 3.11.4 Assignment Compatibility (made bold by me):
S is assignable to a type T, and T is assignable from S, if S has no excess properties with respect to T (3.11.5) and one of the following is true:
[...]
- S is a union type and each constituent type of S is assignable to T.
With regards to the extends keyword, the Typescript handbook states the following (made bold by me):
For practical purposes, type compatibility is dictated by assignment compatibility, even in the cases of the implements and extends clauses.
In your case Foo equates to S in the spec and Bar is your supertype (or superset) which equates to T spec.
However, I am not sure if this answer satisfies you because you can also write bar('x' as Bar); and the compiler won't show an error despite 'x' not being included in your types.
I think your best shot is to simply use function bar(x: Bar). Which then also allows you to call bar('c');