Robert Cooper
Robert Cooper

Reputation: 2240

How do I pass a generic value type accessed through an index in TypeScript?

I"m trying to pass a generic value to a type accessed through an index, but I'm running into a syntax error.

I've got a function type signature named onCreateNode defined on an interface named GatsbyNode:

interface GatsbyNode {
  ...
  onCreateNode?<TNode extends object = {}>(
    args: CreateNodeArgs<TNode>,
    options?: PluginOptions,
    callback?: PluginCallback
  ): void
  ...

I've now created a function that is also named onCreateNode and I want to assign it the type of GatsbyNode['onCreateNode'] while passing a value to the TNode generic:

export const onCreateNode: GatsbyNode['onCreateNode']<AGenericTypeIWantToPassIn> = ...

However, I get a syntax error at the first angle bracket (<) telling me that this is not valid TypeScript.

Upvotes: 6

Views: 1195

Answers (2)

darkbasic
darkbasic

Reputation: 353

In TypeScript it isn't possible to apply type arguments to indexed access types. That's by design because it would require extending the grammar to support multiple sets of type arguments for generic types with generic methods, such as

type Foo<T> = <U>(t: T, u: U) => [T, U];

Thus the following won't work:

class Echo {
  echo = <T>(a: T): T => a;
}
type EchoNumFunc = Echo["echo"]<number>; // Does not work

TypeScript 4.7 introduced instantiation expressions which provide the ability to specify type arguments for generic functions or generic constructors without actually calling them:

function makeBox<T>(value: T) {
    return { value };
};
const makeStringBox = makeBox<string>;  // (value: string) => { value: string }
const stringBox = makeStringBox('abc');  // { value: string }

Exploiting the declare keyword allows you to take advantage of the previously mentioned feature to workaround the issue in the following way:

class Echo {
  echo = <T>(a: T): T => a;
}
declare const echo: Echo["echo"];
type EchoNumFunc = typeof echo<number>; // Works

Upvotes: 3

jcalz
jcalz

Reputation: 328097

You are trying to convert a generic function type to a generic type referring to a function. These are two fundamentally different sorts of generics, and although they are related, the compiler doesn't really give you a direct mechanism to convert between them. See this answer for a fuller explanation; this question is essentially asking for what the other question is asking for.

Anyway, the most straightforward way for you to proceed is to just rewrite the type definition in the desired form yourself:

type GatsbyNodeSpecific<T extends object = {}> = {
  onCreateNode?(args: CreateNodeArgs<T>, options?: PluginOptions, callback?: PluginCallback): void;
}

export const onCreateNode: GatsbyNodeSpecific<AGenericTypeIWantToPassIn>['onCreateNode'] = null!

This will definitely work, but is potentially tedious and possibly error-prone if the upstream definition of GatsbyNode changes.


In the other answer, I show a hacky method to convince the compiler to take a generic function type and convert it to a generic type. For your case it would look like this:

class GenTypeMaker<T extends object = {}> {
  getGatsbyNodeSpecific!: <A extends any[], R>(cb: (...a: A) => R) => () => (...a: A) => R;
  gatsbyNodeSpecific = this.getGatsbyNodeSpecific!(
    null! as NonNullable<GatsbyNode["onCreateNode"]>)<T>()
}
type OnCreateGatsbyNodeSpecific<T extends object> = 
  GenTypeMaker<T>['gatsbyNodeSpecific'] | undefined;

And then you can use this new type like this:

export const onCreateNode2: OnCreateGatsbyNodeSpecific<AGenericTypeIWantToPassIn> = onCreateNode;

This is the same type as before. But I doubt anyone reviewing the code above with a phantom class would be happy with it, and the straightforward-if-redundant method of rewriting the definition manually is probably the better way to go.

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 2

Related Questions