Allow conditional check on generic parameters
See original GitHub issueSuggestion
🔍 Search Terms
List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.
- generics
- constraints
- conditional
- check
✅ Viability 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. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript’s Design Goals.
⭐ Suggestion
Allow conditional constraints on generic parameters.
📃 Motivating Example
- You can constrain generics by type calculation, like this:
type EQ<A, B> = A extends B ? B extends A ? true : false : false
function f<A, B> where [EQ<A,B>] (a:A, b:B) { ... }
f(1,2) // ok
f(1,'a') // err
This describes a function whose two arguments must be of the same type.
The where
field provides an list, each item of this list is a type calculation, and the function can only be called if each item is true
.
- For simplicity, this constraint is only useful when calling the function, and does not affect the judgment of this generic type inside the function.
type IsString<A> = A extends String ? true : false
function f<A> where [IsString<A>] (a:A) {
// Inside the function, don't know that the type of A is a string.
}
f('a') // ok
f(1) // err
- This check is post-processing, that is, when calling the function, the type of the generic type is first deduced through parameters, etc., and finally it is checked whether the constraints in
where
are satisfied. So this won’t affect the existing generic parameter logic.
💻 Use Cases
I think this provides two benefits:
- Can describe generic conditions more freely, not just subtype constraints.
For example, there is a function that expects to enter a phone number:
type IsPhoneNumber<A extends string> = ...
function f<A extends string> where [IsPhoneNumber<A>] (a:A) { ... }
- Allows describing the relationship between multiple generics, and is more expressive.
For example, a function takes two arguments, both of which are items in a given list, and the arguments cannot be duplicates:
type EQ<A, B> = A extends B ? B extends A ? true : false : false
type NOT<A>=A extends false? true: false
type Include<list, item> = list extends [] ? false : list extends [infer x, ...infer xs] ? item extends x ? true : Include<xs, item> : false
type list = ['a', 'b', 'c']
function f<A, B> where [Include<list, A>, Include<list, B>, NOT<EQ<A,B>>] (a:A, b:B) { ... }
My workaround now is to do a type check on the return value of the function and return never
if it doesn’t match the condition:
type EQ<A, B> = A extends B? B extends A? true: false: false
function f<A, B>(a:A, b:B): EQ<A,B> extends true? string: never { ... }
But this has many problems.
First, the return value is inferred as a conditional type and I need to convert it manually:
type EQ<A, B> = A extends B? B extends A? true: false: false
function f<A, B>(a:A, b:B): EQ<A,B> extends true? string: never { return 'a' as any }
Second, when the wrong parameter is entered, the type of the return value is never
, which is different from reporting an error.
Although most of the time, when I try to use a value of type never
, I get an error.
Issue Analytics
- State:
- Created a year ago
- Comments:6 (1 by maintainers)
Top GitHub Comments
@fatcerberus Oh! You are right!
In the case of generic functions calling other generic functions, denying the call is really the only safe way. I’ll give an easier-to-understand example:
Suppose we have two types of calculations,
IsLT5
andIsLT10
, means less than 5 and less than 10. For example,IsLT5<1>
getstrue
andIsLT5<6>
getsfalse
.For
f1(x)
, it is unsafe to consider onlyextends
, such as the case where x is 6.This is really unsatisfactory, but difficult to achieve by narrowing down the generic type with the
where
field.A possible method is to analyze the calling process of the function, and when it is found that
x
inf2
callsf1
, copy thewhere
condition off1
on this parameter tof2
, That is, TS first converts this code internally to:But this seems to complicate the implementation.
I think the easiest way is to not allow the value of the generic type to call a function with a
where
field, but allow the programmer to ignore thewhere
check by casting, for example:In this case,
f1(x)
is safe, because numbers less than 5 must also be less than 10.But it is difficult for TS to know this. We can specify that the
where
check always allows values of typeany
. This allows the programmer to specifyx
as typeany
to ignore thewhere
check. Of course, this requires the programmer to know what himself is doing.This may not be elegant, but I think it’s useful, such type conversions do not pollute the outside of the function, the programmer just has to make sure not to make mistakes here.
@lsby Adding an
extends
constraint doesn’t invalidate my concern. The issue is when one generic function calls other ones, in which case TS can’t verify thewhere
clause for the inner call(s). If TS defaults to allowing the call in this case just because theextends
constraints are met, then that’s not really useful because then the constraint effectively becomesT extends U OR where P<T>
. I’d expect it to be anAND
there instead.