Typescript React generic function type - javascript

I have a case:
interface State {
Mark: boolean;
Jane: boolean;
}
React.Component<{}, State> {
state = {
Mark: false,
Jane: false,
};
fn = (name: string) => () => {
this.setState({ [name]: true });
^^^^^^^^^^
}
render () {
return
['Mark', 'Jane'].map((name) => <div onClick={this.fn(name)}>{name}</div>);
}
Im getting error:
Argument of type '{ [x: string]: boolean; }' is not assignable
to parameter of type 'State | Pick<State, "Mark" | "Jane">
I could do two separate functions for it, but I want to keep it generic. How could I make it generic so the error goes off?

It turns out this is a limitation of the ts compiler itself. Check this issue for more on that: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/26635
Now to solve your problem you can use setState with a callback version. Like bellow:
interface State {
Mark: boolean;
Jane: boolean;
}
type StateKeys = keyof State
class Test extends React.Component <{}, State > {
state = {
Mark: false,
Jane: false,
};
fn = (name: StateKeys) => () => {
this.setState(prev => ({ ...prev, [name]: true }));
}
render() {
return ['Mark', 'Jane'].map((name: StateKeys) => <div onClick={this.fn(name)}>{name}</div>);
}
}

Related

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

Typescript: function parameter that is one of two interfaces

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

Flow error Cannot get `x` because property `y` is missing in `Array` [1]

I have a type guard for state as follow -
/** Type guard for states */
type demo= {
demoList: {[key: number]: Array<{
name?: string,
length?: string,
}>
},
status: boolean
}
I am initializing the state as follow inside my class.
constructor(props: Object) {
super(props);
this.state = {
demoList: {
// eslint-disable-next-line no-useless-computed-key
[0]: {
name: '',
length: '',
}
},
status: false
};
}
And when the user will enter something in the name and length field I update the state as follow inside my onChange method
this.setState({
demoList:
{ ...this.state.demoList, [index]: { name: inputName, length: inputLength} }
});
The error I am getting is when I try to render the value inside render(). I am trying to replace the value in the name or length field by fetching like below
this.state.demoList[index].name
But the error I am getting is
Cannot get `this.state.demoList[index].name` because property `name` is missing in `Array` [1].Flow(InferError)
Any idea what I am doing wrong?
I think you have issue with the Type Guard. You are not using the type guard as per the definition given in its type.
I have created a code-sandbox based on your problem:
https://codesandbox.io/s/quiet-waterfall-c6uu0?file=/src/Demo.tsx
Though for further clarification, you are using your Type Guard type as below:
constructor(props: Object) {
super(props);
this.state = {
demoList: {
// eslint-disable-next-line no-useless-computed-key
[0]: {
name: '',
length: '',
}
},
status: false
};
}
But, as per the type definition of your Type Guard it should be used as:
constructor(props: Object) {
super(props);
this.state = {
demoList: {
// eslint-disable-next-line no-useless-computed-key
[0]: [
{
name: "",
length: ""
}
]
},
status: false
};
}
The value assigned to demoList[0] should be an Array of object.
It should not be:
{
name?: string;
length?: string;
}
It should be:
[{
name?: string;
length?: string;
}]
As well state update should also consider demoList[0] value as Array of object.
It should not be like:
this.setState({
demoList:
{ ...this.state.demoList, [index]: { name: inputName, length: inputLength} }
});
It should be like:
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({
...this.state,
demoList: {
0: [{ name: e.target.value, length: e.target.value.length.toString() }]
}
});
}
Based on how you've defined your state object plus usage, I see you want to create an object with a list of keys as numbers with objects as properties. But what you've done in your demo type is that you defined it as an object of keys as numbers with values as an array of objects.
Below you'll see the changes I've made based on your post with the try flow here. Notice the area I've commented which is the only minor change I had to make.
import React from 'react';
/** Type guard for states */
type demo = {
demoList: {
[key: number]: { // You previously have this as Array<{ ... }> but it should just be an object
name?: string,
length?: string,
},
},
status: boolean,
}
class Comp extends React.Component<{}, demo> {
constructor(props: {}) {
super(props);
this.state = {
demoList: {
// eslint-disable-next-line no-useless-computed-key
[0]: {
name: '',
length: '',
}
},
status: false
};
}
changeState(index, inputName, inputLength) {
this.setState({
demoList:
{ ...this.state.demoList, [index]: { name: inputName, length: inputLength} }
});
}
render() {
const index = 0;
this.state.demoList[index].name
return null;
}
}

React native ref Property 'ref' does not exist on type 'IntrinsicAttributes &

I am getting the following error but I am not able to figure out how to fix it someone can help me out.
Below is also the link on expo with the complete code.
Error on <AppIntroSlider /> which is reported by snack expo
Example:
Type '{ ref: (ref: any) => any; data: { key: string; title: string;
text: string; backgroundColor: string; }[]; renderItem: ({ item }:
any) => Element; renderPagination: (activeIndex: number) => Element;
scrollX: (scrollXList: any) => any; }' is not assignable to type
'IntrinsicAttributes & { data: any[]; renderItem: (info:
ListRenderItemInfo & { dimensions: { width: number; height:
number; }; }) => ReactNode; renderSkipButton?: (() => ReactNode) |
undefined; ... 19 more ...; scrollX?: ((a: any) => void) | undefined;
} & FlatListProps<...> & { ...; }'. Property 'ref' does not exist on
type 'IntrinsicAttributes & { data: any[]; renderItem: (info:
ListRenderItemInfo & { dimensions: { width: number; height:
number; }; }) => ReactNode; renderSkipButton?: (() => ReactNode) |
undefined; ... 19 more ...; scrollX?: ((a: any) => void) | undefined;
} & FlatListProps<...> & { ...; }'.
Link: expo
const slider = useRef(null);
...
<AppIntroSlider
ref={(ref: any) => (slider.current = ref)}
...
type ItemTProps<ItemT> = {
data: ItemT[];
renderItem: (
info: ListRenderItemInfo<ItemT> & {
dimensions: { width: number; height: number };
}
) => React.ReactNode;
renderSkipButton?: () => React.ReactNode;
renderNextButton?: () => React.ReactNode;
renderDoneButton?: () => React.ReactNode;
renderPrevButton?: () => React.ReactNode;
onSlideChange?: (a: number, b: number) => void;
onSkip?: () => void;
onDone?: () => void;
renderPagination?: (activeIndex: number) => React.ReactNode;
activeDotStyle: ViewStyle;
dotStyle: ViewStyle;
dotClickEnabled: boolean;
skipLabel: string;
doneLabel: string;
nextLabel: string;
prevLabel: string;
showDoneButton: boolean;
showNextButton: boolean;
showPrevButton: boolean;
showSkipButton: boolean;
bottomButton: boolean;
scrollX?: (a: any) => void;
} & FlatListProps<ItemT>;
const AppIntroSlider: FunctionComponent<ItemTProps<any>> = ({
data,
renderItem,
renderSkipButton,
renderNextButton,
renderDoneButton,
renderPrevButton,
onSlideChange,
onSkip,
onDone,
renderPagination,
activeDotStyle = {
backgroundColor: 'rgba(255, 255, 255, .9)',
},
dotStyle = {
backgroundColor: 'rgba(0, 0, 0, .2)',
},
dotClickEnabled = true,
skipLabel = 'Skip',
doneLabel = 'Done',
nextLabel = 'Next',
prevLabel = 'Back',
showDoneButton = true,
showNextButton = true,
showPrevButton = false,
showSkipButton = false,
bottomButton = false,
extraData,
scrollX,
...otherProps
}: any) => {
When you log slider.current ref it displayed null because your AppIntroSlider component is a functional component and doesn't support this way. you have two solutions, change AppIntroSlide to a class component and it will work fine, or use forwardRef.
I took a look at the example that you posted with useImperativeHandle and you've got it mostly right. Your usage is a little different than the one in my other answer because your function goToSlide takes arguments.
When you define the interface for the referenced component, you need to define the goToSlide function with the appropriate argument types. You currently defined it as a function that takes no arguments (goToSlide(): void) and that's why you are getting the error "Type '(pageNum: number, triggerOnSlideChange?: boolean | undefined) => void' is not assignable to type '() => void'." on the line with useImperativeHandle.
export interface MyRef {
goToSlide(pageNum: number, triggerOnSlideChange?: boolean): void;
}
A bunch of the props on MyCustomComponentProps should be defined as optional. You are already setting default value for them.
After fixing those two things, all of your errors go away except for those caused by the optional chaining ?.. This is a new-ish feature and I'm not sure how to get Expo to understand it.
You don't technically need the ?. after sliderRef because the ref object will always be defined. It's the current property that might be null, so you do need the ?. after current. But you could also check it the old-fashioned way if the red underlines bother you:
const prev = (activeIndex: number): void => {
if ( sliderRef.current ) {
sliderRef.current.goToSlide(activeIndex - 1, true);
}
};
Updated expo link

Categories