Passing props in React jsx as generics - javascript

In my react app i want to pass a specific interface as a generic into a unspecific component.
For example i have three specific interfaces
SpecificInterfaces.jsx
export interface InterfaceA {
name: string
age: number
...
}
export interface InterfaceB {
name: string
movies: string[]
count: number
...
}
export interface InterfaceC {
name: string
somestuff: someType
}
For each of the interfaces i have a specific component ComponentA, ComponentB and ComponentC.
These Components need to be used in a shared component ComponentShared.
Now for example i want in my ComponentA to return SharedComponent with the generic Type of InterfaceA and props of Type InterfaceA like this:
ComponentA.jsx
export interface Props<T> {
importData: T[]
... some props...
}
const props: Props<InterfaceA> = {
importData: importData //This is from Interface Type InterfaceA
... someProps ...
}
return (
<React.Fragment>
<SharedComponent<InterfaceA> {...props} />
</React.Fragment>
)
And in my sharedComponent i want to access the specific passed generic type like this:
SharedComponent.jsx
const SharedComponent= <T,>({
importData,
...the passed Props
}: Props<T>): JSX.Element => {
importData.map((data: T) =>
data.name)
At importData.map((data:T) => data.name) it throws an error, saying T has no member of name. So i guess something isnt working with my generics i pass in here, because the InterfaceA im passing in as generic has the member "name" like any ohter InterfaceB and InterfaceC. What am i doing wrong?

TypeScript doesn't know anything about the generic inside your function unless you inform it. You need to extend your generic T from a type that has the properties that you use inside the function. Consider this example:
TS Playground
function logNamesBroken <T>(objects: T[]): void {
for (const obj of objects) {
console.log(obj.name);
/* ^^^^
Property 'name' does not exist on type 'T'.(2339) */
}
}
type BaseObject = {
name: string;
};
function logNames <T extends BaseObject>(objects: T[]): void {
for (const obj of objects) {
console.log(obj.name); // ok now
}
}
More, based on the code in your question:
TS Playground
import {default as React} from 'react';
interface BaseItem {
name: string;
}
interface InterfaceA extends BaseItem {
age: number;
}
interface Props<T extends BaseItem> {
importData: T[];
}
const SharedComponent = <T extends BaseItem>({
importData,
}: Props<T>): React.ReactElement => {
return (
<ul>
{
importData.map((data, index) => (
<li key={`${index}-${data.name}`}>{data.name}</li>
))
}
</ul>
);
};
const importData: InterfaceA[] = [{name: 'a', age: 1}, {name: 'b', age: 2}];
const props: Props<InterfaceA> = {
importData,
};
const AnotherComponent = (): React.ReactElement => (
<React.Fragment>
<SharedComponent {...props} />
</React.Fragment>
);

Related

Typescript dynamically infer type from object

I have a JS Object with React components, indexed by ID.
const MODAL_ENTITIES = {
changeEmail: ChangeEmailModal,
changeUsername: ChangeUsernameModal,
};
I would like to have a ModalEntity type which results in this:
type ModalEntity = {
id: 'changeEmail',
props: React.ComponentProps<typeof ChangeEmailModal>
} | {
id: 'changeUsername',
props: React.ComponentProps<typeof ChangeUsernameModal>
};
My problem is, I want the type to be dynamically generated from the MODAL_ENTITIES object, since I want the process of adding a modal to be as effortlessly as possible.
Is there a way to define this type dynamically? I could do this but I want to avoid generics, I would like T to be inferred:
export type ModalEntity<T extends keyof typeof MODAL_ENTITIES> = {
id: T;
props: React.ComponentProps<typeof MODAL_ENTITIES[T]>;
};
I made a mockup. The idea is to get generic T out of your ModalEntity type so that it can be used easily when you add a new modal.
Placeholders for your modals, assuming that each modal has different props:
import React from 'react';
const ChangeEmailModal: React.FC<{ id: string; name: string; email: string }> = ({ id, ...props }) => {
return (
<div id={id}>
{props.name} {props.email}
</div>
);
};
const ChangeUsernameModal: React.FC<{ id: string; otherName: string; username: string }> = ({ id, ...props }) => {
return (
<div id={id}>
{props.otherName} {props.username}
</div>
);
};
const MODAL_ENTITIES = {
changeEmail: ChangeEmailModal,
changeUsername: ChangeUsernameModal
};
Then we get the keys from your MODAL_ENTITIES in a dynamic way:
export type ModalEntities = typeof MODAL_ENTITIES;
// this gets all the keys in type ModalEntities
type StringKeys<T> = {
[k in keyof T]: k;
}[keyof T];
type ModalEntitiesKeys = StringKeys<ModalEntities>;
Finally:
export type ModalEntity = {
[K in ModalEntitiesKeys]: {
id: K;
props: React.ComponentProps<typeof MODAL_ENTITIES[K]>;
};
}[ModalEntitiesKeys];
The ModalEntity type will look like this and it's no longer generic. the type of props fields will be inferred dynamically as you requested regardless of different modal props.
type ModalEntity = {
id: "changeEmail";
props: {
id: string;
name: string;
email: string;
} & {
children?: React.ReactNode;
};
} | {
id: "changeUsername";
props: {
id: string;
otherName: string;
username: string;
} & {
children?: React.ReactNode;
};
}
You can elaborate more on this idea.

How to generate React components dynamically using an array of Props in TypeScript?

I've been tackling an issue of abstracting out some logic for component creation in order to reduce a lot of duplication. As part of this, I have a generic Builder component which I use to dynamically render components baased on the props provided.
The issue comes from the fact that I defined elements as similar to the following:
type InputMap = typeof INPUTS
const INPUTS = {
text: {
component: TextInput,
controlled: false
},
select: {
component: Select
controlled: true
}
}
// Props for TextInput component
type TextProps = {
onChange: (e: ChangeEvent<HTMLInputElement>) => void
onBlur: (e: ChangeEvent<HTMLInputElement>) => void
}
// Props for Select component
type ElementProps = {
onChange: (value: string) => void
onBlur: () => void
}
I want to pass on my fields in a format similar to this:
const fields = [
{
input: "text",
props: {
onChange: e => console.log(e.target.value)
}
},
{
input: "select",
props: {
onChange: value => console.log(value)
}
}
]
This is the type I came up with:
import { ComponentProps } from "react";
export type FieldConfig<T extends FieldValues, K extends keyof InputMap> = {
input: K;
props?: ComponentProps<InputMap[K]["Component"]>
};
However in my Builder component, there's an issue when rendering the component.
<div>
{ fields.map(({ input, props }) => {
const { Component, controlled } = INPUTS[input]
return <Component {...props} /> // ERROR HERE
})}
</div>
const { input, props } = field
TypeScript at that point gives me the following error:
Types of property 'onBlur' are incompatible.
Type 'ChangeHandler' is not assignable to type '() => void'
Is there any way for me to narrow the types from a union to a specific instance of that union in this case? I'm trying my best to avoid any type assertions here. Any help would be greatly appreciated!
You can use a common field interface and a union type to define how your form structure should be handled like this.
interface FieldDefinition<TType extends string, TElement extends HTMLElement> {
input: TType
placeholder?: string
onChange?: React.ChangeEventHandler<TElement>
onBlur?: React.ChangeEventHandler<TElement>
}
interface TextField extends FieldDefinition<'text', HTMLInputElement> {
}
interface SelectField extends FieldDefinition<'select', HTMLSelectElement> {
options: Record<PropertyKey, any>
}
type FormField = TextField | SelectField
const formFields: FormField[] = [
{
input: 'text',
onChange: (event) => console.log(event.target.value)
},
{
input: 'select',
onChange: (event) => console.log(event.target.value),
options: {
foo: 'Foo',
bar: 'Bar',
baz: 'Baz'
}
}
]
This allows it to be properly used when returning the JSX, here's a link to a TypeScript playground showing it used as a component.
This has the added bonus of allowing you to define specific type specific properties that can be defined like an options object for the select input.

Conditional data depends on generic type

Basically I'm using a React function based component.
*** But the question has nothing to do with React specificly.
const Component = <Condition extends boolean>(props: React.PropsWithChildren<Props<Condition>>) => {
Props:
interface Props<Condition extends boolean> {
condition: Condition;
}
In this function, I create a variable to store some data.
const initialValues: Fields<Condition> = (() => {
const base = {
unit: '',
};
if (props.condition) {
return {
...base,
from2: '',
};
}
return base;
})();
The Fields type is configured as following:
interface Base {
unit: string;
}
interface Extended extends Base {
from2: string;
}
export type Fields<Condition extends boolean> = Condition extends true ? Extended : Base;
The entire code organized together:
interface Base {
unit: string;
}
interface Extended extends Base {
from2: string;
}
export type Fields<Condition extends boolean> = Condition extends true ? Extended : Base;
interface Props<Condition extends boolean> extends PropsFromState {
condition: Condition;
}
const Component = <Condition extends boolean>(props: React.PropsWithChildren<Props<Condition>>) => {
const initialValues: IJobFormFields<Condition> = (() => {
const base = {
unit: '',
};
if (props.condition) { // Check if condition (also Condition type) is true
return {
...base,
from2: '',
};
}
return base;
})();
};
The issue is that I receive the following error:
Type '{ unit: string; } | { unit: string; from2: string; }' is not assignable to type 'Fields<Condition>'.
Type '{ unit: string; }' is not assignable to type 'Fields<Condition>'.ts(2322)
That's a current design limitation of Typescript. It cannot narrow the type of conditional type depending on unspecified generic type parameter. And while the type parameter is not explicitly specified the type of Fields<Condition> is opaque to the compiler.
Usually similar cases when function returns a conditional type depending on a generic type parameter are good candidates for rewriting with function overloads. But since you're not returning the value of initialValues I believe you're better off splitting prop generation into separate branches:
const BaseComponent = (props: Base) => null
const ExtendedComponent = (props: Extended) => null
const Component = <T extends boolean>(props: Props<T>) => {
const base = { unit: '' }
if (props.condition) {
return <ExtendedComponent {...base} from2="" />
}
return <BaseComponent {...base} />
};
playground link
You can simply add the property with a question mark making it as an optional property.
unit?: string

Use typescript interfaces for passed props in ReactJs

I created this using typescript:
import React, {FC} from 'react';
interface Interface {
name:string,
age:number
}
const Home: React.FC<Interface> = (info) => {
return (
<div>
<h1>{info.name}</h1>
</div>
);
};
export default Home;
///
const info = {name:'Boris', age:45}
function App() {
return (
<div className="App">
<Home info={info}/>
</div>
);
}
...but i get an error from typescript:
Type '{ info: { name: string; age: number; }; }' is not assignable to type 'IntrinsicAttributes & Interface & { children?: ReactNode; }'.   Property 'info' does not exist on type 'IntrinsicAttributes & Interface & { children?: ReactNode; }'Question: How to avoid this and why it appeared?
This code here:
interface Interface {
name:string,
age:number
}
const Home: React.FC<Interface> = //...
Says that the component Home expects 2 props: name and age.
This code here:
<Home info={info}/>
Passes in one prop named info.
So you either want to pass in name and age as props:
<Home name={info.name} age={info.age}/>
Or you want to declare the info prop:
interface Props {
info: {
name:string,
age:number,
}
}
const Home: React.FC<Props> = ({ info }) => { /* ... */ }
// Pass props like:
<Home info={info}/>
(Note the ({ info }) destructuring assignment, which assigns the info prop the to local variable info.)
Playgorund
You should destruct your props in Home component.
So it should be
const Home: React.FC<Interface> = ({ info }) => {
return (
<div>
<h1>{info.name}</h1>
</div>
);
};

Default prop types through a higher order component

Passing components through a HOC is causing the defaultProps information to be lost to the typescript compiler. For instance
themed.tsx
export interface ThemedProps {
theme: {};
}
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type Subtract<T extends K, K> = Omit<T, keyof K>;
const themed = <P extends ThemedProps = ThemedProps>(
ComponentToWrap: React.ComponentType<P>
) => {
return class ThemeWrappedComponent extends React.Component<
Subtract<P, ThemedProps>
> {
static displayName = `themed(${ComponentToWrap.displayName})`;
theme = () => {
return {}
};
render() {
return (
<ComponentToWrap
{...this.props as P}
theme={this.theme()}
/>
);
}
}
};
Foo.tsx
interface FooProps {
theme: object,
name: string,
}
class Foo extends React.Component<FooProps> {
static defaultProps = {
name: 'world'
}
render() {
return <span>hello ${this.props.name}</span>
}
}
export default themed(Foo);
When I instantiate <Foo />, I get a compiler error saying that Property 'name' is missing in type '{}' but required in type 'Readonly<Pick<FooProps, "name">>'..
I know there's a way to use JSX.LibraryManagedAttributes to get around this kind of thing, but I don't know how, and I can't find any documentation on that feature.
You have to leverage JSX.LibraryManagedAttributes to be able to extract required and optional (defaulted) props from wrapped component in your HOC. This looks somewhat tricky:
import React from 'react';
interface FooProps {
theme: string;
name: string;
}
class Foo extends React.Component<FooProps> {
static defaultProps = {
name: 'world',
};
render() {
return <span>hello ${this.props.name}</span>;
}
}
interface ThemedProps {
theme: string;
}
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Subtract<T extends K, K> = Omit<T, keyof K>;
const themed = <
C extends React.ComponentType<React.ComponentProps<C> & ThemedProps>,
// that's where all the magic happens
ResolvedProps = JSX.LibraryManagedAttributes<C, Subtract<React.ComponentProps<C>, ThemedProps>>
>(
Component: C
) => {
return class ThemeWrappedComponent extends React.Component<ResolvedProps> {
static displayName = `themed(${ComponentToWrap.displayName})`;
render() {
return (
<Component
// proper typecast since ts has fixed type infering on object rest
{...this.props as JSX.LibraryManagedAttributes<C, React.ComponentProps<C>>}
theme="theme"
/>
);
}
};
};
const Wrapped = themed(Foo);
const el = <Wrapped />; // works

Categories