1

I'm writing a Typescript library that handle type-validation.

I have this working code:

type MyTypePredicate<T> = (value: any) => value is T

function createTypePredicateForArrayT<T>(item: T): MyTypePredicate<T[]> {
    return ((value: any) => /* Real code here */ true) as MyTypePredicate<T[]>
}

// This can be use with or without providing the generic T.
const t1 = createTypePredicateForArrayT<number>(42)
const t2 = createTypePredicateForArrayT(42)
// Both t1 and t2 will have the type MyTypePredicate<number[]>

However, purely for consistency with a set of other function in the library, I would prefer to let the provided generic to be same as type as the generic in the returned MyTypePredicate<T>

This can be done with

function createTypePredicateForArrayA<A extends any[]>(item: A[number]): MyTypePredicate<A> {
    return ((value: any) => /* Real code here */ true) as MyTypePredicate<A>
}

// which make this working:
const a1 = createTypePredicateForArrayA<number[]>(42)

// ...but the argument inference stops working,
const a2 = createTypePredicateForArrayA(42)
// it gives a2 the type MyTypePredicate<any[]> and not MyTypePredicate<number[]>

Is there a way to rewrite the second, and still make both a1 and a2 to work?

3
  • 1
    In any event, messing around with inference is more of an art than a science; perhaps this approach meets your needs? If so I'll write up an answer explaining; if not, what am I missing? Commented Jan 15, 2023 at 21:23
  • @jcalz That is a good approach, and it works! Thankyou! Would be even better without a second argument, but guess it is not possible. I have updated the question, and removed X | any, since I understand it can distract this problem. Commented Jan 16, 2023 at 6:52
  • 1
    Okay, I will write up an answer when I get a chance. Commented Jan 16, 2023 at 15:12

1 Answer 1

2

The straightforward approach is to write it your first way, and just let the compiler infer the generic type argument. The additional constraint that the type argument must represent a particular function of the types you care about necessarily complicates things; it's difficult/impossible to have a single type argument work one way with explicit specification and another way with type inference.


One way to get things to work as you request is to add a second type parameter so that you use the first one only for explicit specification and the second one for type inference:

function createTypePredicateForArrayA<
    A extends any[],
    T = A[number]
>(item: T): MyTypePredicate<T[]> {
    return ((value: any) => /* Real code here */ true) as MyTypePredicate<T[]>
}

Let's test it:

const a1 = createTypePredicateForArrayA<number[]>(42)
// const a1: MyTypePredicate<number[]>

const a2 = createTypePredicateForArrayA(42)
// const a2: MyTypePredicate<number[]>

In the a1 case you are explicitly specifying A as number[]. Since TypeScript does not currently support partial type argument inference (as requested in ms/TS#26242), then the T type argument is also specified and not inferred. As such, it ends up falling back to its default of A[number], which means that T is number and you are forced to pass in a number argument for item and you get number[] out.

In the a2 case you are letting the compiler infer A and T. There is no inference site for A, so that just defaults to any[]. But item is an inference site for T, so that defaults to number, and thus the return type is number[].


Another way to do it is to give your function two call signatures; one for type argument inference, and one for manual specification. That is, make it an overloaded function:

function createTypePredicateForArrayA<A extends any[] = never>(
    item: A[number]): MyTypePredicate<A>; // manual
function createTypePredicateForArrayA<T>(item: T): MyTypePredicate<T[]>; // infer

Let's test it:

const a1 = createTypePredicateForArrayA<number[]>(42)
// const a1: MyTypePredicate<number[]>

const a2 = createTypePredicateForArrayA(42)
// const a2: MyTypePredicate<number[]>

In the a1 case you manually specify A as number[], which makes the compiler select the first overload, which behaves as you expect. In the a2 case you do not specify the type argument; the first overload fails because there is no inference site for A, and thus it falls back to the never default, and 42 does not match... so it tries the second overload, and now you get the straightforward inference behavior for T as you expect.


So there you go; two relatively complicated approaches to the relatively complicated problem of struggling against the intended behavior of TypeScript's inference with generic functions.

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

Wow! The secod approch with overloading is solving it all the way! I doubted it was possible. Amazing! Thankyou!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.