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 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)