Levi Hackwith
Levi Hackwith

Reputation: 9332

Why is the first value in my Tuple always unknown?

My Types:

interface Pair<FirstValue, SecondValue> {
    first: () => FirstValue,
    second: () => SecondValue,
    toString: () => string
}

type StateRunner = <V, S>(state: S) => Pair<V, S>;

My Code:

const Pair = <V1, V2>(v1, v2): Pair<V1, V2> => ({
    first: () => v1,
    second: () => v2,
    toString: () => `Pair(${v1}, ${v2})`
});

const myRunner: StateRunner = (state) => Pair("hello", state);

const result = myRunner(someString, someData);

In VSCode, I get this:

description

I'm 100% positive that I'm just not understanding Generics very well (I'm new to TypeScript), but I would think that it would list the result as:

Pair<string, {
    size: string,
    active: boolean
}>

What am I missing? Am I expecting too much from TypeScript/VSCode? The main reason I'm confused is that, when I use Pair directly, intellisense is totally aware of what the return types of first and second will be.

Upvotes: 0

Views: 85

Answers (1)

jcalz
jcalz

Reputation: 329838

Aside: I find myself having to alter your example code just to get to the place where your question exists. Examples: annotate v1 and v2:

const Pair = <V1, V2>(v1: V1, v2: V2): Pair<V1, V2> => ({
  first: () => v1,
  second: () => v2,
  toString: () => `Pair(${v1}, ${v2})`
});

and myRunner takes just one argument:

const result = myRunner({ a: "" }); 

I assume those are what you meant.


The problem with your code is that a function of type StateRunner claims to be able to take a value of generic type S chosen by the caller, and produce a Pair<V, S> for a generic type V also chosen by the caller. That's not possible, is it? Since no value of type V is passed in, there's no plausible way by which the output will be a Pair<V, S>. One symptom of this is that when you let the compiler infer the types that the caller would specify, it has absolutely no idea what V will be. So it infers unknown.


Looking at your implementation, you want S to be chosen by the caller and V to be chosen by the implementer. That means V and S should not be generics of the same kind. I'd expect StateRunner to itself be generic, like this:

type StateRunner<V> = <S>(state: S) => Pair<V, S>;

So V is part of the StateRunner type, chosen by the implementer, and S is part of the generic function type, chosen by the caller. And you won't have a value of type StateRunner; you'll have a value of type StateRunner<V> for a particular V. In the case of myRunner, it will be a StateRunner<string>:

const myRunnerAnnotated: StateRunner<string> = state => Pair("hello", state);

Now things work:

const r = myRunnerAnnotated({ a: "" }); // Pair<string, {a: string}>

If you don't want to manually annotate something as StateRunner<string> you can use a helper function to infer V for you:

const asStateRunner = <V>(s: StateRunner<V>) => s;

And use it like this:

const myRunner = asStateRunner((state) => Pair("hello", state));
// const myRunner: StateRunner<string>

You can see that myRunner is inferred to be a StateRunner<string>, and then result works the way you want as before:

const result = myRunner({ a: "" });  // Pair<string, {a: string}>

Okay, hope that helps give you some direction. Good luck!

Playground link to code

Upvotes: 2

Related Questions