Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic generic type inference for object literals in TypeScript

In typescript, I can declare a generic function like so:

const fn: <T>(arg: T)=>Partial<T>

In this case, TypeScript can sometimes infer the type parameter of the function based on the actual parameters I pass it. Is there a similar way to define a generic object literal whose type parameter can be dynamically inferred based on its contents? Something like:

interface XYZ { 
  obj: <T>{ arr: T[], dict: Partial<T> }
}

I am aware I can make the entire interface generic like so:

interface XYZ<T> {
  arr: T[],
  dict: Partial<T>
}

but I want to avoid that, because then I would have to declare the generic type in advance whenever I am using the interface. For example

const x: XYZ

will not work. If I want to make the declaration general, I am forced to write:

const x: XYZ<any>

but this does not allow TypeScript to dynamically infer the specific generic type based on the actual contents of x

like image 486
prmph Avatar asked May 24 '18 12:05

prmph


People also ask

What is a generic type in typescript?

Many other TypeScript Utility Types, such as ReturnType are generic. ReturnType for instance is able to extract the return type of any function, whatever types used. As you might see, generics can really improve type declarations, as sometimes you don’t know the type of a function or variable in a declaration.

How to use typescript’S type inference?

A better way to do this, is using TypeScript’s type inference to automatically infer the type of X by using a function: ​ Now we can call myComponent.create and TypeScript knows we want something of type { a: number; b: number } as input!

What is a typescript identity function?

A simple example is an identity function, that works for all types: ​ Although not very complex, this function works for every type and ensures that whatever type we pass as input, is the return type of this function. Many other TypeScript Utility Types, such as ReturnType are generic.

How do you define a type with dynamic keys in typescript?

Define a Type for Object with Dynamic keys in TypeScript# Use an index signature to define a type for an object with dynamic keys, e.g. [key: string]: string;. Index signatures are used when we don't know all of the names of a type's properties ahead of time, but know the shape of the values.


2 Answers

Ah, you want generic values as discussed in Microsoft/TypeScript#17574. As you note, they don't exist in the language except in the case of generic functions. You can go give a 👍 to that issue if you want, or discuss your use case if you think it's helpful.

Given the generic interface

interface XYZ<T> {
  arr: T[],
  dict: Partial<T>
}

I would just use this workaround: Make a generic function to verify that a value is XYZ<T> for some T, and allow type inference to actually infer T whenever it is necessary. Never try to declare something of type XYZ. Like this:

const asXYZ = <T>(xyz: XYZ<T>) => xyz;

const x = asXYZ({
  arr: [{ a: 1, b: 2 }, { a: 3, b: 4 }],
  dict: { a: 1300 }
}); // becomes XYZ<{a: number, b: number}>

The above usually works for me in practice. The pro is that it's "natural" TypeScript. The con is it doesn't represent the "I don't care what type T is" properly.


If you really want, you could define an existential type. TypeScript doesn't natively support these, but there is a way to represent it:

interface SomeXYZ {
  <R>(processXYZ: <T>(x: XYZ<T>) => R): R
}
const asSomeXYZ = <T>(xyz: XYZ<T>): SomeXYZ => 
  <R>(processXYZ: <T>(x: XYZ<T>) => R) => processXYZ(xyz);

The SomeXYZ type is a concrete type that doesn't care anymore about T, but holds a reference to XYZ<T> for some T. You use asSomeXYZ to create one from an object:

const someXYZ: SomeXYZ = asSomeXYZ({
  arr: [{ a: 1, b: 2 }, { a: 3, b: 4 }],
  dict: { a: 1300 }
}); // SomeXYZ

And you use it by passing a function that processes the held reference. That function has to be ready for XYZ<T> for any T, since you don't know what type of T a SomeXYZ is holding.

// use one
const xyzArrLength = someXYZ((xyz => xyz.arr.length))

The xyzArrLength is a number, since the function xyz => xyz.arr.length returns a number no matter what T is.

Existential types in TypeScript are awkward, since there's a lot of inversion of control going on. That's the major downside to this, and why I usually go with the less-perfect-but-easier-to-think-about workaround I presented first.

Hope that helps. Good luck!

EDIT: re-reading your question makes me think you’re actually asking for the answer I listed as a “workaround”. So, uh... use that? Cheers.

like image 50
jcalz Avatar answered Oct 13 '22 00:10

jcalz


interface MyGenericObjectLiteral<T> {
  arr: T[],
  dict: Partial<T>
}

interface XYZ { 
    obj: MyGenericObjectLiteral<any>
}

Interface XYZ here will not be generic, just the subobject MyGenericObjectLiteral. It is actually just what you desired, except that it has a bit different syntax.

like image 28
Kit Isaev Avatar answered Oct 12 '22 23:10

Kit Isaev