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.
Related
I built a Select component in react that is fully typed and I now added a multiple prop to it, that will change the typing of the value and the onChange callback to be of type Array<T>
What the multiple prop does is it uses what I believe is called a Distributive Conditional to determine the type of both value and onChange of the Component's props like this:
interface SelectBaseProps<T extends SelectValue = string> {
options: SelectOption<T>[];
placeholder?: string;
disabled?: boolean;
className?: string;
searchable?: boolean;
}
export type SelectProps<T extends SelectValue, TMultiple extends boolean = false> = SelectBaseProps<T> &
(TMultiple extends false
? {
multiple?: TMultiple;
value: T;
onChange: (value: T) => void;
}
: {
multiple?: TMultiple;
value: T[];
onChange: (value: T[]) => void;
});
Where SelectValue just limits the values to be of type string or number for now.
I know, not the prettiest implementation but this is already after some iterations of debugging.
And then the select component itself is basically just
export default function Select<T extends SelectValue, TMultiple extends boolean = false>(...) {...}
Now, on first sight this seems to work just fine! If I use this in a test component like this:
function SelectTest() {
const [val, setVal] = useState<string>();
const options: SelectOption<string>[] = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
];
return <>
<Select value={val} options={options} onChange={(x) => console.log(x)} />
{/* However, this works! */}
<Select value={val} options={options} onChange={setVal} />
</>;
}
and hover over the onChange prop, it clearly says that the prop of the onChange callback is of type string. If I change the code and make value be an array, the onChange value is also of type array. But for some reason, the type is not infered in the function passed into the callback and typescript complains that Parameter 'x' implicitly has an 'any' type.
So my question: Why is typescript not able to infer the type here, even though the function is typed correctly and can infer the type even for custom string types?
It could be related to my tsconfig configuration, so I added it in the reproduction stackblitz:
https://stackblitz.com/edit/react-ts-wuj3yu?file=Select.tsx,tsconfig.json
Take a look: https://stackblitz.com/edit/react-ts-agwgkq?file=Select.tsx,App.tsx
I have to refuse of defining so many types/props:
import React, { useState } from 'react';
export type SelectValue = string | number | Array<string> | Array<number>;
type Unwrap<T> = T extends Array<infer R> ? R : T;
export interface SelectOption<T extends SelectValue> {
label: string;
value: Unwrap<T>;
}
interface SelectProps<T extends SelectValue> {
options: SelectOption<T>[];
value: T;
onChange: (value: T) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
searchable?: boolean;
}
export function Select<T extends SelectValue>(props: SelectProps<T>) {
return <div>Dummy Select</div>;
}
...
import * as React from 'react';
import { Select } from './Select';
import './style.css';
export default function App() {
const [val, setVal] = React.useState<string>('');
const options = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
];
const [multipleVal, setMultipleVal] = React.useState<Array<string>>([]);
return (
<React.Fragment>
<Select value={val} options={options} onChange={(x) => console.log(x)} /> // x is string
{/* However, this works! */}
<Select
value={multipleVal}
options={options}
onChange={(x) => console.log(x)} // x is Array<string>
/>
</React.Fragment>
);
}
Code
App.tsx
function App(){
return(
<>
<Slider products={products} />. // Error
</>
}
export default App;
Slider.tsx
import { useSelector, connect } from "react-redux";
import { ProductType, ProductItem, StateType } from "types";
const Slider = ({ products, number }: any) => {
// const number = useSelector((state: StateType) => state.number);
return (
<S.Slider>
<S.SliderWrapper widthSize={number}>
{products}
...
<S.SliderWrapper>
</S.Slider>
);
};
export default connect((state: any) => {
console.log(state.number);
return { number: state.number };
}, null)(Slider);
type.ts
export interface ProductItem {
id: number;
title: string;
price: number;
bgcolor: string;
colors: { code: string; img: any }[];
}
export interface ProductType {
products: ProductItem[];
}
Currently, Slider Component is using useSelector, but an error occurs when trying to change it to connect. So, how should I change the connect part and how should I modify the parameter and type of the slider component?
Error Message
TS2322: Type '{ products: { id: number; title: string; price: number;
bgcolor: string; colors: { code: string; img: any; }[]; }[]; }' is not
assignable to type 'IntrinsicAttributes & Omit<any, string | number |
symbol>'. Property 'products' does not exist on type
'IntrinsicAttributes & Omit<any, string | number | symbol>'.
Solution
Slider.tsx
const Slider = ({ number, products }: any) => {
...
...
}
export default connect((state: StateType, { products }: ProductType) => {
return { number: state.number, products };
}, null)(Slider);
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>
);
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
I was looking at this question, which I thought was related to my issue. However, it is a bit different from my use case.
I have a function called parseScanResults takes an argument that is an object. The object can be one of two types. However, typescript is throwing an error with the below code:
const ScanForm: React.FC<IScanFormProps> = ({ children, onSubmit, parseScanResults }) => {
const [scannerActive, toggleScannerActive] = useState(false);
const closeScanner = (): void => {
toggleScannerActive(false);
};
const handleScanResults = (results: IVoucherScanResults | IBlinkCardScanResults): void => {
const { cardString, stringMonth, stringYear } = parseScanResults(results);
setValue('cardNumber', cardString);
setValue('expMonth', stringMonth);
setValue('expYear', stringYear);
toggleScannerActive(false);
};
return (
<Form onSubmit={handleSubmit(onSubmit)}>
{children({ scannerActive, closeScanner, handleScanResults })}
</Form>
);
};
import CreditCardBarcodeScanner from 'src/components/scanners/credit_card_barcode_scanner';
import { IVoucherScanResults, IScannerProps, IParsedScanResults } from '../scanners/card_scanners';
import ScanForm from './scan-form';
function CreditCardBarcodeForm(): JSX.Element {
const onSubmit = (data: { expMonth: string; expYear: string; securityCode: string; cardNumber: string }): void => {
// Do something with form data
console.log(data);
};
const parseScanResults = (results: IVoucherScanResults): IParsedScanResults => {
const { text } = results;
const [cardString, expirationString] = text.slice().split('/');
const stringMonth = expirationString.slice(0, 2);
const stringYear = expirationString.slice(2, 4);
return { cardString, stringMonth, stringYear };
};
return (
<ScanForm onSubmit={onSubmit} parseScanResults={parseScanResults}>
{({ scannerActive, closeScanner, handleScanResults }: IScannerProps) => (
<CreditCardBarcodeScanner
scannerActive={scannerActive}
closeScanner={closeScanner}
handleScanResults={handleScanResults}
/>
)}
</ScanForm>
);
}
export default CreditCardBarcodeForm;
export interface IBlinkCardScanResults {
cardNumber: string;
cvv: string;
expiryDate: {
day?: number;
empty?: boolean;
month: number;
originalString?: string;
successfullyParsed?: boolean;
year: number;
};
}
export interface IVoucherScanResults {
text: string;
timestamp: number;
format: number;
numBits: number;
}
export interface IParsedScanResults {
cardString: string;
stringMonth: string;
stringYear: string;
}
export interface IScannerProps {
scannerActive: boolean;
closeScanner: () => void;
handleScanResults: (results: IVoucherScanResults | IBlinkCardScanResults) => void;
}
export interface IScanFormProps {
children: (props: ICardScannerProps) => React.ReactElement;
onSubmit: (data: { expMonth: string; expYear: string; securityCode: string; cardNumber: string }) => void;
parseScanResults: (results: IBlinkCardScanResults | IVoucherScanResults) => IParsedScanResults;
}
The error states:
Type '(results: IVoucherScanResults) => IParsedScanResults' is not assignable to type '(results: IBlinkCardScanResults | IVoucherScanResults) => IParsedScanResults'.
Types of parameters 'results' and 'results' are incompatible.
Type 'IBlinkCardScanResults | IVoucherScanResults' is not assignable to type 'IVoucherScanResults'.
Type 'IBlinkCardScanResults' is missing the following properties from type 'IVoucherScanResults': text, timestamp, format, numBitsts(2322)
Your problem is that parseScanUtils is a either a function that gets an IVoucherScanResults as a parameter, or a function that gets an IBlinkCardScanResults as a parameter, while only one is true. in this case it looks like your component is receiving the first of the two.
the main point is that there is a difference between having a union of functions where each one gets a specific parameter type and having one function whose parameter is a union of two types.
parseScanResults:
((results: IBlinkCardScanResults) => IParsedScanResults)
| ((results: IVoucherScanResults) => IParsedScanResults);
vs.
parseScanResults:
((results: IBlinkCardScanResults | IVoucherScanResults) => IParsedScanResults)
EDIT
what you can do is use a generic and instead of typing your component function you can explicitly type your parameter:
let's first make the interface generic:
export interface IScanFormProps<T extends IBlinkCardScanResults | IVoucherScanResults> {
children: (props: ICardScannerProps) => React.ReactElement;
onSubmit: (data: { expMonth: string; expYear: string; securityCode: string; cardNumber: string }) => void;
parseScanResults: (results: T) => IParsedScanResults;
}
than you can update your functional component like this:
const ScanForm = <T extends IBlinkCardScanResults | IVoucherScanResults>({ children, onSubmit, parseScanResults }: T) => {
and your handleScanResults function:
const handleScanResults = (results: T): void => {
...rest of code...
}
then all that's left to do is call the component with the wanted type (example for IBlinkCardScanResults):
<ScanForm<IBlinkCardScanResults> onSubmit={onSubmit} parseScanResults={parseScanResults}>
I believe it should work now