question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Allow conditional check on generic parameters

See original GitHub issue

Suggestion

🔍 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

  1. 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.

  1. 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
  1. 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:

  1. 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) { ... }
  1. 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:open
  • Created a year ago
  • Comments:6 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
lsbycommented, Nov 3, 2022

@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:

type IsLT5<A> = ...
type IsLT10<A> = ...

function f1<A extends number> where [IsLT5<A>](x: A) {
    // ...
}

function f2<A extends number> where [IsLT10<A>](x: A) {
    f1(x)
}

Suppose we have two types of calculations, IsLT5 and IsLT10, means less than 5 and less than 10. For example, IsLT5<1> gets true and IsLT5<6> gets false.

For f1(x), it is unsafe to consider only extends, 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 in f2 calls f1, copy the where condition of f1 on this parameter to f2, That is, TS first converts this code internally to:

type IsLT5<A> = ...
type IsLT10<A> = ...

function f1<A extends number> where [IsLT5<A>](x: A) {
    // ...
}

function f2<A extends number> where [IsLT10<A>, IsLT5<A>](x: A) {
    f1(x)
}

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 the where check by casting, for example:

type IsLT5<A> = ...
type IsLT10<A> = ...

function f1<A extends number> where [IsLT10<A>](x: A) {
    // ...
}

function f2<A extends number> where [IsLT5<A>](x: A) {
    f1(x as any)
}

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 type any. This allows the programmer to specify x as type any to ignore the where 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.

0reactions
fatcerberuscommented, Nov 2, 2022

@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 the where clause for the inner call(s). If TS defaults to allowing the call in this case just because the extends constraints are met, then that’s not really useful because then the constraint effectively becomes T extends U OR where P<T>. I’d expect it to be an AND there instead.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Conditional method parameters based on Generic type
One way to do it would be using tuples in rest parameters in a separate public signature: type OneArgFn<T> = T extends void...
Read more >
Documentation - Conditional Types - TypeScript
Create types which act like if statements in the type system.
Read more >
Constraints on type parameters - C# Programming Guide
Constraints tell the compiler what capabilities a type argument ... it can allow methods of that type to be called in the generic...
Read more >
Generic Parameters conditional check - Rust Users Forum
Say I have: fn <T: Foo>() { } How can I add a conditional in the function to check what Foo is?
Read more >
Generics: in, out, where | Kotlin Documentation
Due to the type erasure, there is no general way to check whether an instance of a generic type was created with certain...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found