Reputation: 5264
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
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>
},
})
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.
Upvotes: 15