Repository navigation
Feature Request: "extends oneof" generic constraint; allows for narrowing type parameters #27808
Description
Search Terms
- generic bounds
- narrow generics
- extends oneof
Suggestion
Add a new kind of generic type bound, similar to T extends C but of the form T extends oneof(A, B, C).
(Please bikeshed the semantics, not the syntax. I know this version is not great to write, but it is backwards compatible.)
Similar to T extends C, when the type parameter is determined (either explicitly or through inference), the compiler would check that the constraint holds. T extends oneof(A, B, C) means that at least one of T extends A, T extends B, T extends C holds. So, for example, in a function
function smallest<T extends oneof(string, number)>(x: T[]): T {
if (x.length == 0) {
throw new Error('empty');
}
return x.slice(0).sort()[0];
}Just like today, these would be legal:
smallest<number>([1, 2, 3); // legal
smallest<string>(["a", "b", "c"]); // legal
smallest([1, 2, 3]); // legal
smallest(["a", "b", "c"]); // legalBut (unlike using extends) the following would be illegal:
smallest<string | number>(["a", "b", "c"]); // illegal
// string|number does not extend string
// string|number does not extend number
// Therefore, string|number is not "in" string|number, so the call fails (at compile time).
// Similarly, these are illegal:
smallest<string | number>([1, 2, 3]); // illegal
smallest([1, "a", 3]); // illegalUse Cases / Examples
What this would open up is the ability to narrow generic parameters by putting type guards on values inside functions:
function smallestString(xs: string[]): string {
... // e.g. a natural-sort smallest string function
}
function smallestNumber(x: number[]): number {
... // e.g. a sort that compares numbers correctly instead of lexicographically
}
function smallest<T extends oneof(string, number)>(x: T[]): T {
if (x.length == 0) {
throw new Error('empty');
}
const first = x[0]; // first has type "T"
if (typeof first == "string") {
// it is either the case that T extends string or that T extends number.
// typeof (anything extending number) is not "string", so we know at this point that
// T extends string only.
return smallestString(x); // legal
}
// at this point, we know that if T extended string, it would have exited the first if.
// therefore, we can safely call
return smallestNumber(x);
}This can't be safely done using extends, since looking at one item (even if there's only one item) can't tell you anything about T; only about that object's dynamic type.
Unresolved: Syntax
The actual syntax isn't really important to me; I just would like to be able to get narrowing of generic types in a principled way.
(EDIT:)
Note: despite the initial appearance, oneof(...) is not a type operator. The abstract syntax parse would be more like T extends_oneof(A, B, C); the oneof and the extends are not separate.
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript / JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. new expression-level syntax)
(any solution will reserve new syntax, so it's not a breaking change, and it only affects flow / type narrowing so no runtime component is needed)
Activity
mattmccutchen commentedon Oct 11, 2018
The use case sounds like a duplicate of #24085, though it seems you've thought through more of the consequences. Let's close this in favor of #24085.
DanielRosenwasser commentedon Oct 11, 2018
Sounds like you want an "exclusive-or" type operator - similar to #14094.
Nathan-Fenner commentedon Oct 12, 2018
It is close to a combination of #24085 and #25879 but in a relatively simple way that ensures soundness is preserved.
It only affects generic instantiation and narrowing inside generic functions. No new types or ways of types are being created; an xor operator doesn't do anything to achieve this.
jack-williams commentedon Oct 12, 2018
That is what a union constraint does.Do you not mean exactly one of?mattmccutchen commentedon Oct 12, 2018
No because if
T = A | B | Cthen none ofT extends A,T extends B,T extends Cholds. (Were you readingextendsbackwards?)jack-williams commentedon Oct 12, 2018
True! No I wasn't, but I had parsed it in my head like
(T extends A) | (T extends B) | (T extends C).michaeljota commentedon Apr 29, 2019
What about a XOR type operator? This would be useful in other scenarios as well. As
|is a valid bitwise operator in JS for OR, it would fit using^as XOR operator in types.Nathan-Fenner commentedon Apr 29, 2019
Michael De Abreu (@michaeljota) See previous comments for why that doesn't work. There is no type today that could be put inside the
extendsto get the desired behavior. Therefore, new type operations cannot solve this problem, since they just allow you (potentially conveniently) write new types. The typenumber ^ stringis exactly the same asstring | number, since there is no overlap betweenstringandnumber. It does nothing to solve this problem.This has to be solved by a different kind of constraint rather than a different type in the
extendsclause.michaeljota commentedon Apr 29, 2019
You want one of a giving list of types, right? I really don't know much about types, sorry. Still, OR operator can be satisfied with n types of the interception, and a XOR operator should only allow one of the types in the interception.
I understand that if you create a interception,
string | number, you can use only the functions of both types, and you should narrow down to one of those to use those functions. Also, if you declare a generic type asT extends string | number, thenTwould acceptstring,numberorstring | number. Then a XOR operator used in a generic type asT extends string ^ numbershould only accept strictly one ofstringornumber, excluding the interception itselfstring | number.That's not what you would like this to do?
Nathan-Fenner commentedon Apr 29, 2019
The problem with that approach is that the
^is incoherent anywhere outside of an "extends" clause, so it becomes (pointless) syntactic sugar.For example, what should
do? Are you allowed to reassign it to
"hello"? Or, in more-complicated examples, like:why should/shouldn't this be allowed?
There's no coherent way that this can be done. The only way to give it meaning is to only allow it in
extendsclauses, where it has the meaning ofIt still represents a new type of constraint, not a type operator (because it doesn't produce coherent types).
michaeljota commentedon Apr 29, 2019
I think it should. In this case, I guess this would behave the same as
string | number.Here it would be useful, as you either want an array of strings, or an array of numbers, but not both. I think, this would be something like:
Playground link
jcalz commentedon Jun 6, 2019
This would make #30769 less of a bitter pill to swallow...
67 remaining items
craigphicks commentedon Dec 8, 2023
Ruben Tytgat (@angryzor)
This port concern TypeScript behavior and are not at all a criticism of you or your ideas.
1
As you said, the typescript compiler will infer
string | numberfor this type declaration case:But if we write it as a type function,
it does not lose precision. For the distribution over the union functionality to work, the function form is required.
2
Another issue: there is some strange TypeScript behavior with the
&calculations:TypeScript seems to delay the calculation of
&at times - as it does here.Personally I think it would be better to immediately see
AandA1should be the same, right?BandB1should be the same, right?A1andB1together imply thatnever[]and(string & number)[]both extend each other, which I believe should mean they are equal. However they are not equal.never[] extends (string & number)[]should be true (A1), according to set theory.(string & number)[] extends never[]should be false (B1), according to set theory.Conclusion
TypeScript handling of types at sets is not really mathematically precise.
At least, there are a few corner cases that I think TypeScript does not properly handle (as of V5.3.3).
emilioplatzer commentedon Dec 9, 2023
I think that this cannot be closed yet. It is unsolved.
Han Seoul-Oh (@laughinghan), your given example fixes the problem of using a function that allows T1 or T2 but not T1|T2 union.
In my opinion that is not the problem (because it has a solution). The problem is implementing that function. Inside that function you cannot have a generic type
Tthat can beT1orT2but notT1|T2. That is specialy bad for numeric types.See my example in my previous comment: #27808 (comment)
laughinghan commentedon Dec 11, 2023
Emilio (@emilioplatzer) Thank you, that's a much better example. Can we update the main issue description then? Both to mention the better example, and to mention that function overloads are an (incomplete) workaround. The fact that the best workaround requires annoying casts is a pretty good argument for this feature:
More generally TypeScript doesn't really track correlations between types much at all (with distributive conditional types being a very limited exception); this does seem like potentially a small, practical way to specify type correlations in some useful cases.
craigphicks commentedon Dec 11, 2023
Han Seoul-Oh (@laughinghan) Emilio (@emilioplatzer) Nathan Fenner (@Nathan-Fenner)
There are really 3 separate items that are related:
returnstatements therein.Item 3 is already working, item 2 is not.
Notice that Emilio (@emilioplatzer) post says
That is talking about item 3.
The OP #27808 title implies that a new form for specifying correlations (
extends oneof) is the main feature - that feature belongs to item 1. However, the OP long content also talks about item 3.nikelborm commentedon Oct 25, 2024
ts playground
Isn't it just a syntax sugar for writing more function overloads? You can achieve the same functionality with a few type helpers:
(Thanks to Joe Calzaretta (@jcalz) for UnionToIntersection magic on stackoverflow)
Helpers
Definition
Tests and usage
Example 1
Definition
Tests and usage
Example 2 (to show applicability to the OP's example)
Definition
Tests and usage
TheRealMkadmi commentedon Oct 27, 2024
EVA (Entity Value Attribute) (@nikelborm) excellent answer.
here's a slightly more flexible version that would accept the render method as parameter:
lgarron commentedon Oct 28, 2024
EVA (Entity Value Attribute) (@nikelborm) and Wahib Mkadmi (@TheRealMkadmi): I don't have time to test out your implemenations, but it's cool to hear that this is already possible to some extent. Could I interest you in contributing to https://github.com/sindresorhus/type-fest/ so that it's easy for others to use your implementations? 😄
marcospgp commentedon Jun 27, 2025
this would be a great addition for when one wants to constrain a type with
keyofbut also ensure it is always a single key, and not something like"key1" | "key2"codpro2005 commentedon Oct 13, 2025
I’m sure a similar solution already exists, but something close to this is already possible:
Edit: to provide a bit more context, this is equivalent to the following:
codpro2005 commentedon Oct 14, 2025
While presumably much more niche, it's worth noting that
nofalso seems to work reasonably well:NOf Implementation
lyle45 commentedon Oct 20, 2025
RyanCavanaugh commentedon Oct 20, 2025
Please don't post enormous AI summaries. Leave that to me 🙂
lyle45 commentedon Oct 21, 2025
😢
Maybe used Ai to articulate but it was very well thought out. Went over what's written many many times to make sure it's accurate... Understandable though why it might be disruptive...
Can I repost my version of implementation without the gibergaber though? Might be helpful for others
Really worked hard on this post 😭
Would love if at least you could go over it and see if there's at least a bit if merit to it and if it has possibly new approaches or value
RyanCavanaugh commentedon Oct 21, 2025
I'm not really sure what you're trying to accomplish with it, so I can't speak to how to better achieve that goal
irfanstract commentedon Aug 27, 2026
a possible solution is to make (the simple identifer)
oneofa special marker type with saidone-ofsemantics:C1 | C2 | C3 | ...s (sinceextends oneof<[C1, C2, C3, ...]>mathematically impliesextends C1 | C2 | C3- as others pointed out)and may additionally be flagged (
illegal 'oneof' application)NoInferandThisType.interface C extends Opaque<CBase>(Support some non-structural (nominal) type matching #202)