3

I have a method that takes a string parameter. However, I want to constrain that parameter a little bit. So, I create a union type with the strings I want to accept:

type Foo = 'a' | 'b';

now, I have a simple method:

function bar(foo: Foo) {
    // do something with that string
}

What I want to do is allow a caller to pass other strings (besides a and b). However, I want them to sort of extend the original union type.

function bar<T extends Foo>(str: T) {
    // do something with that string
}
type Bar = Foo | 'c';
// this doesn't work: Argument of type 'Bar' is not assignable to parameter of type 'Foo'. Type '"c"' is not assignable to type 'Foo'.
bar('c' as Bar); 

Is there any way to express this constraint in TypeScript?

3
  • If you can pass in any string how is it different from string? Commented Nov 19, 2018 at 21:06
  • 1
    @TitianCernicova-Dragomir in the OP he said "other", not "any". He wants to extend the set of allowed strings. Commented Nov 19, 2018 at 21:45
  • Possible duplicate of Extending union type alias in typescript? Commented Nov 20, 2018 at 0:32

1 Answer 1

2

@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');

Sign up to request clarification or add additional context in comments.

1 Comment

Foo & 'c' is equivalent to never (no string can be both "c" and also either "a" or "b"). It's unlikely that is the intent here. More likely, the question is using extends incorrectly; instead of T extends Foo, the intent is more like T super Foo, but that doesn't exist in TypeScript.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.