Dynamic generic type inference for object literals in TypeScript

99封情书 提交于 2020-03-18 05:07:06

问题


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


回答1:


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.




回答2:


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.



来源:https://stackoverflow.com/questions/50509952/dynamic-generic-type-inference-for-object-literals-in-typescript

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!