How to create conditional types in typescript? - reactjs

So I have a Form component in react which takes props to handle various different situations. As I am using it for an online store it will mostly be sending input data to the server, and it needs to work with zod and infered tRPC typings. and I made this generic type, which I thought would mean I can pass either login data or signup data and it would work either way.
export type FormType<T extends SignUpForm | LoginForm> = T extends SignUpForm ? SignUpForm : LoginForm;
Instead I get this error in the form component
value={form[types]} // form refers to state object
// const form: SignUpForm | LoginForm Element implicitly has an 'any' type
// because expression of type 'string' can't be used to index type 'SignUpForm | LoginForm'
and this error in the parent element
onResponse={handleRequest} // onResponse refers to Forms callback hook
Type '(data: SignUpForm) => Promise<void>' is not assignable to type
'(data: SignUpForm | LoginForm) => Promise<void>'.
Types of parameters 'data' and 'data' are incompatible.
Type 'SignUpForm | LoginForm' is not assignable to type 'SignUpForm'.
Type 'LoginForm' is missing the following properties from type 'SignUpForm':
firstname, lastname, day, month, yearts(2322)
and the types of each is just an object with strings as values.
This app was also created using create-t3-app if that helps.
I'm not sure If I've explained this well so please feel free to edit this post or ask questions
types.ts
export interface SignUpForm {
firstname: string;
lastname: string;
email: string;
password: string;
day: string;
month: string;
year: string;
}
export interface LoginForm {
email: string;
password: string;
}
export type FormType<T extends SignUpForm | LoginForm> = T extends SignUpForm ? SignUpForm : LoginForm;
hook.tsx
const useSetForm = (component: FormType<SignUpForm | LoginForm>) => {
// component = object, is either SignupForm or LoginForm
const [value, setForm] = useState(component);
return [value, (event: React.ChangeEvent<HTMLInputElement>) => {
setForm((oldValue) => {
return { ...oldValue, [event.target.name]: event.target.value };
});
},
] as const;
};
parent.tsx
const Signup: FC<SignupProps> = ({ }): JSX.Element => {
const handleRequest = async (data: FormType<SignUpForm>) => {
const res = await client.mutation('mongo.sign-up', data);
console.log(res)
}
return (
<Layout>
<div className=''>
<Form
formData={{ firstname: '', lastname: '', email: '', password: '', }}
target={'mongo.sign-up'}
buttons={['day', 'month', 'year']}
onResponse={handleRequest} />
{/** pass types to create input fields, pass endpoint to target, buttons only needs definition for date of birth elements */}
{/** onResponse returns complete form obj */}
</div>
</Layout>
)
}
export default Signup
FormComponent.tsx
interface FormProps {
formData: FormType<SignUpForm>
target: `mongo.${'sign-up' | 'login'}` | `sendgrid.send-email`;
onResponse: (data: SignUpForm | LoginForm) => Promise<void>
};
export const Form: FC<FormProps> = ({ target, onResponse, formData }): JSX.Element => {
const [form, setForm] = useSetForm(formData);
const handleCallback = useCallback((data: SignUpForm | LoginForm) => {
onResponse(data)
// pass data to parent elements
}, [onResponse])
return (
<>
<form onSubmit={handleCallback}>
<>
{Object.keys(formData).map((types) => {
/** type is passed from parent elements, */
return (
<input
key={types}
name={types}
value={form[types]}
type={types}
placeholder={types}
onChange={setForm} />)
//renders form text input elements
})}
</>
<button type='submit' disabled={false}> Submit </button>
</form>
</>
)
}

Related

How to specify return type of a reusable useForm hook?

I'm trying to make a reusable useForm hook to use it on two kinds of forms. First kind is a form with just inputs so its type is:
type Input = {
[key: string]: string;
}
Second kind is a form with inputs and selects so its type something like this:
type Input = {
[key: string]: string | { [key: string]: string };
};
Hook accepts initial value. For example for form with inputs and selects I will provide this init value:
const INIT_INPUT = {
name: '',
description: '',
category: {
id: '',
name: '',
},
};
How can I specify return type from hook? I'm sorry it's difficult to explain what I need. I will try on example:
Hook
type Input = {
[key: string]: string | { [key: string]: string };
};
const useForm = (initInput: Input, callback: () => void) => {
const [input, setInput] = useState(initInput);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setInput((prevState: any) => ({
...prevState,
[name]: value,
}));
};
const handleChangeSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value, name } = e.target;
const id = e.target.selectedOptions[0].getAttribute('data-set');
setInput((prevState: any) => ({
...prevState,
[name]: {
name: value,
id,
},
}));
};
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
callback();
};
return { input, setInput, handleChange, submit, handleChangeSelect };
};
export default useForm;
Component with form:
On each input I get an error because TS doesn't know if input.name is a string or a object. How to fix that without as string on each input?
TS error
Type 'string | { [key: string]: string; }' is not assignable to type
'string'. Type '{ [key: string]: string; }' is not assignable to
type 'string'.
const INIT_INPUT = {
name: '',
description: '',
price: '',
image: '',
weight: '',
brand: '',
category: {
id: '',
name: '',
},
};
const SomeForm: React.FC = () => {
const { input, setInput, handleChange, submit, handleChangeSelect } = useForm(INIT_INPUT, submitHandler);
return (
<>
<input value={input.name || ''} onChange={handleChange} />
<input value={input.description || ''} onChange={handleChange} />
//...other inputs
</>
);
};
This is actually a question about TypeScript than purely React, but let's go.
You can solve this by using generics. So, you'll want to add a type parameter to the hook that extends an object ({} or Record<string, Something>) and then use that same type as the return type for the hook. For example:
const useForm = <T extends {}>(initInput: T, callback: () => void):
{
input: T,
// the other output types
} => {
// rest of your hook
}
Also, for the typing of the input data to be more precise, mark it as as const:
const INIT_INPUT = {
name: '',
description: '',
price: '',
image: '',
weight: '',
brand: '',
category: {
id: '',
name: '',
},
} as const;
So that way, the returned type will be inferred to be the same as the input's type, and you'll know the type of each individual property.
Finally, some React tips is to wrap each function inside the hook in a useCallback to avoid unnecessary re-renders, as well as the submitHandler that is an argument to the hook.

How to type a Form component with onSubmit?

I've got my own Form component which I want to keep separated from the Parent Component.
const Form: React.FC<any> = ({ children, handleFormSubmit }) => (
<form onSubmit={handleFormSubmit}>{children}</form>
);
Parent Component
const ParentComponent = () => {
...
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
publishReview(review).then(() => setReview(initialReviewState));
};
return (
<Form onSubmit={handleFormSubmit}>
...
</Form>
)}
I thought handleFormSubmit would get the types from its declaration const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>).
But it doesn't
I've tried to build an interface and even with an any type:
interface FormProps {
handleFormSubmit: any
}
const Form: React.FC<FormProps>= ({ children, handleFormSubmit }) => (
<form onSubmit={handleFormSubmit}>{children}</form>
);
But it gave the below error:
const Form: React.FunctionComponent<FormProps>
Type '{ children: Element; onSubmit: (event: FormEvent<HTMLFormElement>) => void; }' is not assignable to type 'IntrinsicAttributes & FormProps & { children?: ReactNode; }'.
Property 'onSubmit' does not exist on type 'IntrinsicAttributes & FormProps & { children?: ReactNode; }'.
This worked for me. Just guessing based on your guess. Thanks!
handlePostForm = (e: React.FormEvent) => {
You would need to specify the type of the form elements. Let's say for example in your form you have an input with id yourInputName:
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="yourInputName">Username:</label>
<input id="yourInputName" type="text" />
</div>
<button type="submit">Submit</button>
</form>
In your handleSubmit, you would need to specify the type of the event.currentTarget. A form tag has a type of HTMLFormElement with a property elements, which are a type of HTMLFormControlsCollection, but we can override that to tell TS what our input is. Create an interface which extends the HTMLFormControlsCollection, specify the type of your field and then use the custom interface to override the elements property in the HTMLFormElement.
interface FormElements extends HTMLFormControlsCollection {
yourInputName: HTMLInputElement
}
interface YourFormElement extends HTMLFormElement {
readonly elements: FormElements
}
Then you pass that new type to your handling function
const handleFormSubmit = (e: React.FormEvent<YourFormElement>) => {}
You can now access the value of your field and typescript will know what it is
const handleFormSubmit = (e: React.FormEvent<YourFormElement>) => {
e.preventDefault();
console.log(e.currentTarget.elements.yourInputName.value)
}
If you hover on value, TS should show you a message that the data type is HTMLInputElement.value: string, just as you have specified in your FormElements interface.
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
if you have uncontrolled form in react( i used MUI field) :
<form onSubmit={handleSubmit}>
<TextField
error={isError}
helperText={isError ? 'Incorrect entry.' : ''}
margin="normal"
color="success"
label="Name and surname"
id="nameSurname"
size="small"
/></form>
you can make this:
const handleSubmit = (e: SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
nameSurname: { value: string };
//password: { value: string };
};
const nameSurname = target.nameSurname.value;
console.log(`nameSurname`, nameSurname);
};
inspired from here
You've got a mistake in your prop names. Do you see the mismatch?
const Form: React.FC<any> = ({ children, handleFormSubmit }) => (
<Form onSubmit={handleFormSubmit}>
Your Form component takes a prop named handleFormSubmit, but you are calling it with a prop named onSubmit instead.
You need to change the name in one of those places so that they both match. Either call <Form handleFormSubmit={handleFormSubmit}> or change the props of the Form component so that the prop is named onSubmit.
Here's how it should look if you rename the prop to onSubmit and give it a proper type instead of any.
interface FormProps {
onSubmit: React.FormEventHandler;
}
const Form: React.FC<FormProps> = ({ children, onSubmit }) => (
<form onSubmit={onSubmit}>{children}</form>
);
You do not need to make any changes in your ParentComponent if you define Form like this.
As Ivo wrote, but in case you want to properly type it with type:
type FormElements = Readonly<
{
yourInputName: HTMLInputElement
} & HTMLFormControlsCollection
>;
type YourFormElement = Readonly<
{
elements: FormElements;
} & HTMLFormElement
>;

Form in React and TypeScript with DRY handle of change

I looking for best way manage state of form in React with TypeScript. My simple form have two values: login and password fields. I implement IState interface for form state and DRY handleChange method for store new value in state without recreating function in each render() execution.
interface IState {
login: string;
password: string;
}
class LoginForm extends React.Component<{}, IState> {
public state = {
login: "",
password: "",
};
public render() {
const { login, password } = this.state;
return (
<form>
<input name="login" value={login} onChange={this.handleChange}/>
<input name="password" value={password} onChange={this.handleChange} type="password"/>
</form>
);
}
private handleChange = (e: React.FormEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget;
const n = name as keyof IState;
// #ts-ignore
this.setState({
[n]: value,
});
}
}
I use native HTML's name attribute for store field name. n variable will have only login or password value. Any other value is impossible. Can I tell TypeScript the n variable is "login" | "password" literals type? TypeScript regard n as string type variable even I use:
const n = name as keyof IState;
or
const n = name as "login" | "password";
Then, when I remove // #ts-ignore I get error:
Type error: Argument of type '{ [x: string]: string; }' is not assignable to parameter of type 'IState | Pick<IState, "login" | "password"> | ((prevState: Readonly<IState>, props: Readonly<IProps>) => IState | Pick<IState, "login" | "password"> | null) | null'.
Type '{ [x: string]: string; }' is missing the following properties from type 'Pick<IState, "login" | "password">': login, password TS2345
but no error when I hardcode:
const n = "login";
Any way to force "login" | "password" type to n variable? Or any other solution for DRY handleChange without optimization issues and pure TypeScript?
We can use Pick to ensure that you're setting a key that has been defined in your IState interface.
private handleChange = (e: React.FormEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget;
this.setState({
[name]: value
} as Pick<IState, keyof IState>);
};
Or alternative you can use Partial which will make all your state keys optional.
class App extends React.Component<{}, Partial<IState>> {
// ... rest of component
}
1.Declare your interface
interface IState {
login: string;
password: string;
}
2.Declare your state
const [myState, setMyState] = useState<IState>()
3.Declare your onchange function
const onChange=(e: any): void => {
const { name, value } = e.currentTarget;
setMyState({ ...myState, [name]: value });
}
4.Declare your markup
<input name="login" type="text" onChange={onChange}/>
We are using the spread function to maintain and update the state Note that you need to name the markup element with the "name" attribute and it should correspond with your interface attribute
Spread syntax
React naming elements

Can't handle typescript with react and redux

I am new at typescript and really struggling. I can't understand where to begin and where to end . Yes , there are many recourses on internet , but i couldn't manage to get that information and use in my project . Hope to meet some help here. I have also done some typechecking and if you find something that i could have done better , please just tell me to improve it.
So now i am struggling with redux mapStateToProps and mapDispatchToProps . I have tried many variants , but every time i have got some kind of errors.
I will post my code which represents my dashboard component which is connected to state .
import * as React from 'react';
import { connect } from 'react-redux';
import SearchIcon from '../SvgIcons';
import MapComponent from '../Map';
import { getEventInfo, getUserInfo } from '../../actions';
interface StateProps {
userReducer: {
accessToken: string
},
eventReducer: {
events: object[]
}
}
interface DispatchProps {
dispatchUserInfo: () => void;
dispatchEventInfo: (accessToken: string, query: string) => void;
}
interface OwnProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void,
accessToken: string,
events: object[]
}
type Props = StateProps & DispatchProps & OwnProps;
class DashboardPage extends React.Component<Props, {}> {
componentDidMount() {
const { dispatchUserInfo } = this.props;
dispatchUserInfo();
}
handleEventSearch = e => {
e.preventDefault();
const { dispatchEventInfo, accessToken } = this.props;
const query: string = e.target.children[0].value;
dispatchEventInfo(accessToken, query);
}
render() {
const { events } = this.props;
return (
<div className="dashboard-container">
<div className="search-event">
<form className="search-event__form" onSubmit={this.handleEventSearch}>
<input
autoComplete="off"
type="text"
name="search-event"
placeholder="Search an event"
className="search-event__input"
aria-label="Enter search text"
/>
<button type="submit" className="search-event__button">
<SearchIcon />
Search
</button>
</form>
<p className="sign-out">
Sign out
</p>
</div>
<div className="google-map">
<MapComponent events={events} />
</div>
</div>
);
}
}
const mapStateToProps = (state: StateProps) => {
const accessToken = state.userReducer.accessToken || '';
const events = state.eventReducer || [];
return {
accessToken,
events
};
};
const mapDispatchToProps = (dispatch: DispatchProps) => ({
dispatchEventInfo(query: string, token: string) {
dispatch(getEventInfo(query, token));
},
dispatchUserInfo() {
dispatch(getUserInfo());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage);
These are typescript errors
1) Refers tohandleEventSearch`method
[ts] Parameter 'e' implicitly has an 'any' type.
2) refers to mapDispatchToProps
[ts] Cannot invoke an expression whose type lacks a call signature. Type 'DispatchProps' has no compatible call signatures.
3) refers to mapDispatchToProps in connect HOC
Argument of type '(dispatch: DispatchProps) => { dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }' is not assignable to parameter of type 'MapDispatchToPropsParam<{ dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }, {}>'.
Type '(dispatch: DispatchProps) => { dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }' is not assignable to type 'MapDispatchToPropsFunction<{ dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }, {}>'.
Types of parameters 'dispatch' and 'dispatch' are incompatible.
Type 'Dispatch>' is not assignable to type 'DispatchProps'.
Property 'dispatchUserInfo' is missing in type 'Dispatch>'.
Also if you can provide me very good sources to learn about react and redux with typescript , so i could easily write my code.
You need to supply event type explicitly, any or
React.SyntheticEvent<...something>
Dispatch parameter should be Dispatch type from Redux
Also a little tip, you can define your props like:
ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps> & OwnProps
and remove unnecessary interfaces

Is it possible to infer the type of component properties based on parent component?

I have this container
export interface State {
email: string
}
const mapStateToProps = (state: State) => ({
email: state.email,
})
const mapDispatchToProps = (dispatch: Dispatch<Action>) => ({
onChange: (name: string, value: string) => dispatch(/*...*/),
})
export default connect(mapStateToProps, mapDispatchToProps)(Login)
and this component
export interface LoginProps {
email: string
onChange: (name: string, value: string) => void
}
const Login = (props: LoginProps) => (
<p>Render something here</p>
)
Is there a way to infer the type of the login properties based on the Container definition so I don't have to manually define LoginProps ?
There are good type definitions for connect, so the types of the props passed to the wrapped component can be inferred. If you wrote the container/component like this...
export interface State {
email: string
}
const stateToProps = (state: State) => ({
email: state.email
})
const dispatchToProps = (dispatch: Dispatch<Action>) => ({
onChange: (name: string, value: string) => dispatch(/*...*/)
})
const BlahBlah = connect(stateToProps, dispatchToProps)(({ email, onChange }) => (
<div />
));
email and onChange are typed correctly without additional annotation.
However, when you write the Login component separately, without type annotation...
const Login = (props) => (
<p>Render something here</p>
)
the type of props cannot be inferred. This is because in addition to passing Login to connect, you could also call it directly. For example, I could write in a component:
render() {
return (
<Login unrelated="a string" typescript="cool" />
);
}
Without annotation on the Login component, there is no way for the compiler to know which of the invocations of Login (the connected one, or my direct rendering) provides the correct props. Without annotation then the compiler can only type props as any, so we lose type-safety.

Resources