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"]); // legal
But (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]); // illegal
Use 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 codeThis wouldn't change the runtime behavior of existing JavaScript codeThis could be implemented without emitting different JS based on the types of the expressionsThis 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.
ghost commentedon Oct 11, 2018
I don't think that's it since
string
andnumber
are already exclusive so astring xor number
type wouldn't be distinguishable fromstring | number
. It's more like they want two overloads:But not
string | number
becausesmallest([1, "2"])
is likely to be an error.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 | C
then none ofT extends A
,T extends B
,T extends C
holds. (Were you readingextends
backwards?)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
@michaeljota See previous comments for why that doesn't work. There is no type today that could be put inside the
extends
to get the desired behavior. Therefore, new type operations cannot solve this problem, since they just allow you (potentially conveniently) write new types. The typenumber ^ string
is exactly the same asstring | number
, since there is no overlap betweenstring
andnumber
. 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
extends
clause.150 remaining items