3

First of all, sorry for the bad title. I really don't know how to describe the problem, and maybe that's why I haven't found a solution yet!

Here's a small snippet demonstrating my problem:

type Type<T> = {
  key: keyof T,
  doStuff: (value: T[typeof key]) => void
//                          ^^^
// TS2034: Cannot find name 'key'.
};

It's pretty straightforward what I'm trying to do (I hope). I've tried to solve this several times, but every time the parameter ended up being an union of all available types.

const Test: Type<{ var1: string, var2: number }> = {
  key: 'var1',
  doStuff: (value) => {}
//          ^^^^^
// (parameter) value: string | number
};

I'd appreciate it a lot if somebody could help me with this one. If you need additional info about what I'm trying to do here, or what I've already tried, please let me know!

houfio
  • 457
  • 2
  • 7
  • 13
  • 1
    There are no built-in [existential types](https://github.com/Microsoft/TypeScript/issues/14466) in TypeScript, so the only way to do this is to make `Type` generic in both `T` and `K extends keyof T`. – jcalz Jul 24 '18 at 15:46

2 Answers2

8

There are no built-in existential types in TypeScript, so you can't say "I don't care which key key is but it needs to be some key of T".

The only way to do this is to make Type generic in both T and K extends keyof T, like this:

type Type<T, K extends keyof T> = {
  key: K,
  doStuff: (value: T[K]) => void
};

Then you can specify Test like this:

const Test: Type<{ var1: string, var2: number }, "var1"> = {
  key: 'var1',
  doStuff: (value) => { } // value is inferred as string
}

This works but maybe you're unhappy about having to specify "var1" manually in both the type and the key property. Unfortunately you can't just specify T and leave K to be inferred, at least for now. Eventually there should be a way to have partial type argument inference, possibly as soon as August 2018, for TypeScript 3.1.

For now you can do a workaround involving currying, where a generic function returns a generic function. You specify one and leave the other to be inferred. Like this:

const typeFor = <T>() => <K extends keyof T>(type: Type<T, K>) => type;

// T is specified manually
const typeForVar1StringVar2Number = typeFor<{ var1: string, var2: number }>();

// K is inferred from the argument
const Test2 = typeForVar1StringVar2Number({
  key: 'var1',
  doStuff: (value) => { } // value is inferred as string
});

It's a bit more involved but it does save you from writing out 'var1' for K.

Okay, hope that helps. Good luck!


Edit: I see that you really do want existential types since you need an array of these things. One way to get existential-like types when you have a union of literals (like keyof T if T doesn't have a string index) is to use distributive conditional types:

type PossibleTypes<T> = keyof T extends infer K ? 
  K extends any ? Type<T, K> : never : never;

PossibleTypes<T> becomes a union of all Type<T, K> for every K in keyof T. Let's use it to make that array:

type ArrayOfPossibleTypes<T> = Array<PossibleTypes<T>>
const asArrayOfPossibleTypes = <T>(arr: ArrayOfPossibleTypes<T>) => arr;
const testArray = asArrayOfPossibleTypes<{ var1: string, var2: number }>([
  {
    key: 'var1', doStuff(value) { /* value is string */ }
  }, {
    key: 'var2', doStuff(value) { /* value is number */ }
  }
]);

The inference looks good there. If you're not too afraid of distributive conditional types I'd say that could work for you.


If all else fails, there is an implementation of existential types in TypeScript, but it involves continuation passing which is probably too much for your use case. I'll include an example for you for completeness:

type ExistentialType<T> = <R>(f: <K extends keyof T>(x: Type<T, K>) => R) => R;

Instead of saying "I'm a Type<T, K> for some K", it's saying "If you give me a function which operates on any Type<T, K>, I can call that function for you and give you the result". Let's create one:

const exType: ExistentialType<{ var1: string, var2: number }> = 
  (f) => f({ key: 'var1', doStuff(value) { } });

And then use it:

const obj = {var1: "hey", var2: 123};
exType(function (t) { if (t.doStuff) t.doStuff(obj[t.key]) });

The inside-out nature of this is wacky and fun, and occasionally useful... but probably overkill here.


Okay, hope that helps. Good luck again.

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for the reply! Sadly this doesn't really fix my problem, but that's my own fault. I forgot to mention that I want to use the type with the doStuff function as an array (eg. https://pastebin.com/gbDrNxSp). I suppose that's not possible right now? – houfio Jul 24 '18 at 19:33
  • Depends what you mean by "possible". Updated. – jcalz Jul 24 '18 at 20:38
0

If you don't mind putting the key in the type parameters:

export type Type<T, V extends keyof T> = {
    key: V, // not required
    doStuff: (value: T[V]) => void
};

const Test: Type<{ var1: string, var2: number }, "var1"> = {
    key: "var1", // not required
    doStuff: (value): void => {
        // value is type string
    }
};
Graeme Wicksted
  • 1,770
  • 1
  • 18
  • 21