Eliav Louski
Eliav Louski

Reputation: 5264

Typescript - infer generic type from object prop value

I read a lot, and found similar things but not exactly what I want.

I want to define a type by using a prop value inside an object and use that information to define other property inside this object.

JSX.IntrinsicElements includes definitions for all react elements. let's focus on the SVG elements circle and path for this example.

so lets try define the type defention:

export type svgCustomType<T extends "path" | "circle"> = {
  svgElem: T;
  svgProps?: { [key in keyof JSX.IntrinsicElements[T]]: JSX.IntrinsicElements[T] };
};

here I try to get the type value T(which can be "path" or "circle") and use this value to define the appropriate properties type for svgProps.(can be React.SVGProps<SVGCircleElement> or React.SVGProps<SVGPathElement>

example usage:

const test: svgCustomType = {  //Error: "Generic type 'svgCustomType' requires 1 type argument(s)", why?
  svgElem: "path",
  svgProps: {
    cx: 10,  // next step: should be error here because cx is not defined in `SVGPathElement`
  },
};

Why do I get Generic type 'svgCustomType' requires 1 type argument(s)? I want to get this type from svgElem value.

i will just mention that using const test: svgCustomType<"path"> = {...} will work and cx will not be accepted.

for clearly:
I'm not sure if is even possible in typescript.
I'm writing a react lib and i want my user to be able to get IntelliSense suggestions while typing,and if the user running typescript(and not javascript) he will also will get type errors.
let's say my lib component have prop svgHeadProp with type svgCustomType.
so i want the user to be able to write:

svgHeadProp={svgElem:"path",svgProps:{...}}

and not:

svgHeadProp<"path">={svgElem:"path",svgProps:{...}}

because not all my users uses typescript(and for the ones that does, its annoying specifying type for a prop )

Upvotes: 7

Views: 4879

Answers (1)

Oblosys
Oblosys

Reputation: 15106

The issue is that to use SvgCustomTypeGeneric in a type signature, you'll have to pass that type parameter. Using SvgCustomTypeGeneric<'path' | 'circle'> won't work, as this will just allow every occurrence of T to be 'path' or 'circle' without any dependency between them. What will work though is a union type, SvgCustomTypeGeneric<'path'> | SvgCustomTypeGeneric<'circle'>, which can be created from the 'path' | 'circle' union.

To show how it works, let's rename the generic function to SvgCustomTypeGeneric, and use JSX.IntrinsicElements[T] as the type of svgProps (otherwise the values are nested, e.g. svgProps: cx: {cx: 10})):

type SvgKey = 'path' | 'circle'

export type SvgCustomTypeGeneric<T extends SvgKey> = {
  svgElem: T;
  svgProps?: JSX.IntrinsicElements[T]
}

To create a union type of SvgCustomTypeGeneric types, you can use a mapped type to create an object with for each key the corresponding SvgCustomTypeGeneric type, and extract the values from this:

type SvgCustomType = {[K in SvgKey]: SvgCustomTypeGeneric<K>}[SvgKey]
//  evaluates to: SvgCustomTypeGeneric<"path"> | SvgCustomTypeGeneric<"circle">

When testing it, the cx property is not helpful as it is also allowed on an SVGPathElement, but the ref property requires a correctly typed value, and can be used instead:

const test: SvgCustomType[] = [{
    svgElem: 'path',
    svgProps: {
      cx: 10, // No error, SVGPathElement can have 'cx' properties.
      ref: createRef<SVGPathElement>(),
    },
  }, {
    svgElem: 'circle',
    svgProps: {
      cx: 10,
      ref: createRef<SVGCircleElement>(),
    },
  }, { // ERROR
    svgElem: 'circle',
    svgProps: {
      cx: 10,
      ref: createRef<SVGPathElement>(),
    },
  },
]

An alternative solution is to create a generic constructor function to validate the objects. It's a bit simpler and gives prettier error messages, but does require adding actual code rather than just types:

const mkSvg =<K extends SvgKey>(x: SvgCustomTypeGeneric<K>) => x

const test = mkSvg({
  svgElem: 'circle',
  svgProps: {
    ref: createRef<SVGPathElement>(),
    // Type 'RefObject<SVGPathElement>' is not assignable to type 'LegacyRef<SVGCircleElement> 
  },
})

TypeScript playground

UPDATE: It's also possible to write SvgCustomType as a one-liner, by using conditional types to introduce the type variable and distribute it over the union:

export type SvgCustomTypeOneLiner =
  SvgKey extends infer T ? T extends SvgKey ? {
    svgElem: T;
    svgProps?: JSX.IntrinsicElements[T]
  } : never : never

And if you really want to go all out, you can even drop the dependency on SvgKey:

type SvgCustomTypeUltimate = 
  keyof JSX.IntrinsicElements extends infer T ? T extends keyof JSX.IntrinsicElements ? {
    svgElem: T;
    svgProps?: JSX.IntrinsicElements[T]
  } : never : never

Both these types have the same behavior as SvgCustomType defined above.

TypeScript playground

Upvotes: 15

Related Questions