7

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 )

2
  • 1
    There's a solution to get the types to match, but it won't give the error on cx, as React.SVGProps<SVGPathElement> does have a cx property for some reason (tsplay.dev/WoqolN). Commented Apr 21, 2021 at 12:32
  • (The reason you get an error for svgCustomType<"path"> is that because of the svgProps typing, you need to use svgProps: {cx: {cx: 10}}, which is accepted for both paths and circles.) Commented Apr 21, 2021 at 13:18

1 Answer 1

15

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

Sign up to request clarification or add additional context in comments.

5 Comments

just a great answer! this is what I've been looking for! thanks a lot!
You're welcome :-) About the one-liner, it's possible but not pretty. Let me add it to the answer.
great update friend! wish I could vote up twice!
this is like magic (in a good way)
Say you would like to have an object type in place of SVGKey, how would you approach that? This fails with the union type, as only string | number | symbol are allowed as property keys.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.