TypeScript React: Use an array of strings as an index and get values from state - reactjs

import React, { useState } from 'react';
interface PropsInterface {
keys: string; // name,email,password
}
const Poster: React.FC<PropsInterface> = (props: PropsInterface) => {
const [state, setState] = useState({
name: 'jim',
email: 'jim#gmaail.com',
password: 'secret',
loading: false,
keys: props.keys.split(','), // [ name, email, password ]
});
const { keys } = state;
I need to somehow get the values for each of the keys and form a string that will end up looking like this
"name=jim&email=jim#gmaail.com&password=secret"
I thought I could simply use URLSearchParams() that quickly became a problem with TypeScript. My next thought was to loop over the keys and then get the corresponding values from the state
for (const i in keys) {
if (keys.hasOwnProperty(i)) {
console.log(state[keys[i]]);
}
}
which then gives me the error
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ auth: boolean; baseUrl: string; keys: string[]; method: string; }'.
No index signature with a parameter of type 'string' was found on type '{ auth: boolean; baseUrl: string; keys: string[]; method: string; }'.ts(7053)

Fundamentally, when you do state[keys[i]], you're using a string to index into state. If your state object's type doesn't have an index signature, TypeScript won't let you do that (without a type assertion)...
You can use a local type assertion, perhaps with an alias variable, instead:
const rawState = state as any as Record<string, string>;
for (const key of keys) {
console.log(rawState[key]);
}
(Also note using for-of to loop the array.)
Of course, that's disabling type checking for that loop...
Live on the playground

As I understand, it's maybe your solution to solve the problem.
import * as React from "react";
import { render } from "react-dom";
interface PropsInterface {
keys: string; // name,email,password
}
const Poster: React.FC<PropsInterface> = (props: PropsInterface) => {
const [state, setState] = React.useState({
name: 'jim',
email: 'jim#gmaail.com',
password: 'secret',
loading: false,
keys: props.keys.split(','), // [ name, email, password ]
});
const pairKeys = React.useMemo<string>(() => {
const order = ["name", "email", "password"];
const result: string[] = [];
state.keys.forEach((key: string, index) => {
result.push(`${order[index]}=${state[order[index]]}`)
});
return result.join("&")
}, [state.keys])
return (
<div>{JSON.stringify(pairKeys, null, 2)}</div>
)
}
render(<Poster keys="John,test#test.com,testpass" />, document.getElementById("root"));

Related

Reducer with typing reported that it can only accept never type

I'm trying to make a slice with a typed reducer.
First I define IFeatureState and initialState like so:
interface IFeatureState {
content: string;
open: boolean;
}
const initialState: IFeatureState = {
content: "",
open: false,
};
Then I write the reducer:
type StatePayload<T> = {
[K in keyof T]: {
propName: K;
value: T[K];
};
}[keyof T];
export default function setState<T>(
state: T,
{ payload }: PayloadAction<StatePayload<T>>
) {
state[payload.propName] = payload.value;
}
If I try to test this as a function it throws no errors:
const statePayload : StatePayload<IFeatureState> = {propName: 'content',value: 'asd'};
const testReducer = setState(initialState, {payload: statePayload, 'type':'...'});
// ok
But if you try to create a slice using the function ``createSlice` I got an error:
export const appSlice = createSlice({
name: "app",
initialState,
reducers: {
setState,
},
});
appSlice.actions.setState(statePayload);
// Argument of type '{ propName: "content"; value: string; }' is not assignable to parameter of type 'never'.ts(2345)
What am I doing wrong?
StatePayload<T> seems overcomplicated, I would simplify it a bit:
type StatePayload<T> = {
propName: string
value: T
}
now, you need to satisfy generic type here :
const statePayload: StatePayload<boolean> = {
propName: 'content',
value: true
}
above refactor will satisfy TypeScript, so it no longer complains in example you provided. However if your goal is to have type safety that will be avare that statePayload.propname that has string value "open" should also have statePayload.value of type boolean and if statePayload.propname has value "content" then statePayload.value should have type boolean... oh boy it is even complicated to wirte down in plain english so let's use TypeScript instead :)
import { createSlice, PayloadAction } from '#reduxjs/toolkit'
interface IFeatureState {
content: string
open: boolean
}
const initialState: IFeatureState = {
content: '',
open: false
}
type StatePayload<K extends keyof IFeatureState> = {
propName: K,
value: IFeatureState[K]
}
export default function setStateAction(
state, // state type will be inferred by TS
{ payload }: PayloadAction<StatePayload<'content' | 'open'>>
) {
state[payload.propName] = payload.value
}
const statePayload: StatePayload<'content'> = {
propName: 'content',
value: "STRINNG EXAMPLE"
}
const statePayload2: StatePayload<'open'> = {
propName: 'open',
value: true
}
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
setState: setStateAction
}
})
const testReducer = appSlice.reducer
const { setState } = appSlice.actions
setState(statePayload)
setState(statePayload2)

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.

Generic UseForm Hook

I'm trying to create a useForm hook with generics and Typescript.
I'd like the consumer to be able to enter an interface and initial state, and then for the hook to dynamically create a new state with added validation fields.
For example, the user passes in:
const {formState, handleSubmit, updateField} = useForm<{
email: string;
}>({
email: "",
});
And behind the scenes the hook would create a state like this:
{
email: {
value: "",
validationMessage: "",
validationType: "none" // as opposed to "success" or "error"
}
}
I'm trying to achieve this with reduce, with the following code:
interface FormField {
value: string,
validationType: ValidationProperty,
validationMessage: string
}
function isInInitialState<T>(val: string | keyof T, vals: T): val is keyof T {
return typeof val === "string" && Object.keys(vals).includes(val)
}
function useForm<T>(initialFormState: T) {
const [formState, setFormState] = useState<Record<keyof T, FormField>>(Object.keys(initialFormState).reduce<Record<keyof T, FormField>>((previous, current) => {
return {
...previous,
[current]: {
value: isInInitialState(current, initialFormState) ? previous[current] : "",
validationType: "none",
validationMessage: ""
}
}
}, {}));
...rest of hook.
The problem for Typescript is that my starting value of an empty object is not the same shape as T, and so the compiler throws an error. Likewise, if I populate the starting value with the same values as initialFormState, I have the same problem.
In short, how can I tell Typescript that I'm going to definitely end up with Record<keyof T, FormField>>, and so it doesn't need to worry about the starting value?
Using a type assertion will be helpful in your case. The reason this will be necessary is that the return type of Object.keys() is always string[] in TypeScript. This is because TS is structurally-typed, and therefore objects can have excess properties beyond the ones you've defined in your type.
It is also useful to extract your transformation function in this case:
TS Playground
import {useState} from 'react';
type ValidationType = 'error' | 'none' | 'success';
type FormField = {
validationMessage: string;
validationType: ValidationType;
value: string;
};
function transformObject <T extends Record<string, string>>(o: T): Record<keyof T, FormField> {
return Object.keys(o).reduce((previous, key) => ({
...previous,
[key]: {
value: o[key as keyof T],
validationType: 'none',
validationMessage: '',
}
}), {} as Record<keyof T, FormField>);
}
function useForm <T extends Record<string, string>>(initialFormState: T) {
const [formState, setFormState] = useState(transformObject(initialFormState));
// rest of hook...
}
transformObject({email: 999}); // Desirable compiler error
// ~~~~~
// Type 'number' is not assignable to type 'string'.(2322)
const transformed = transformObject({email: 'recipient#name.tld'}); // ok
console.log(transformed); // Result:
// {
// "email": {
// "value": "recipient#name.tld",
// "validationType": "none",
// "validationMessage": ""
// }
// }
I think you can use type assertions for your issue.
Sometimes typescript is not able to infer types properly, this is what happen when you call your reduce function. So you may tell typescript what will be the expected return for your constant.
type ValidationProperty = 'none' | 'other'
interface FormField {
value: string
validationType: ValidationProperty
validationMessage: string
}
type FormState<T> = Record<keyof T, FormField>
function useForm<T>(initialFormState: T) {
const keys = Object.keys(initialFormState)
const defaultFormState = keys.reduce(
(acc, key) => ({
...acc,
[key]: {
value: initialFormState[key],
validationType: 'none',
validationMessage: ''
}
}),
{}
) as FormState<T> // Type assertion here
const [formState, setFormState] = useState<FormState<T>>(defaultFormState)
return [formState]
}
const MyComponent = () => {
const [form] = useForm({ email: 'string' })
// form has the type FormState<{email: string}>
return <div>Hello</div>
}

Typescript and context, Type is not assignable

Hello I'm learning typescript and I'm having an error of Type, this is component catcontext.tsx:
import { createContext } from "react";
interface structCat {
id: number,
idParent: number,
description: string
};
const CatContext = createContext<structCat | null>(null);
export default CatContext;
and this globalstate.tsx:
import CatContext from './CatContext';
import CatReducers from './CatReducers';
import { useReducer } from 'react';
import Data from '../../../Data/Data.json';
const initialState = {
cats: Data,
}
function GlobalState(props: any){
const [ state, dispatch ] = useReducer(CatReducers, initialState);
const AddCat = (cat: any) => {
dispatch({
type: ADD_CAT,
payload: cat
});
}
return(
<CatContext.Provider
value={{
cats: state.cats,
AddCat
}}
>
{props.children}
</CatContext.Provider>
)
}
export default GlobalState;
this is error:
Type '{ cats: any; AddCat: (cat: any) => void; }' is not assignable to type 'structCat'.
Object literal may only specify known properties, and 'cats' does not exist in type 'structCat'. TS2322
the Data.json struct is like:
[
{
"id": 1,
"idParent": null,
"description": "main"
},
{
"id": 2,
"idParent": 1,
"description": "name 1"
}
]
so, I'm trying to create project using context api and typescript, so the type context should be type struct Data.json, I'm not sure is that way is rigth, but my idea is create a struct type that I can add, edit, delect, search and list data.
{ cats: any; AddCat: (cat: any) => void; } doesn't equal { id: number, idParent: number, description: string }
additional notes:
cat should be type - structCat
cats should be type - array of structCat
Default context shouldn't be null if it can be avoided or isn't really a valid value for the purpose.
AddCat should be addCat - it's good practice to only capitalize the first letter of react component names
interface ICatContext {
cats: structCat[];
addCat: (cat: structCat) => void;
}
createContext<ICatContext>({
cats: [],
addCat: (_)=>{}
})

Typescript and Context API

I am trying to implement Typescript and Context API together in an application. I am facing the issue of implementing the function of deleteMovie and addMovie into the MovieContext.Provider's value prop.
Here's the error that I'm receiving:
Type '{ movies: MovieAttribute[]; deleteMovie: (id: number) => void; addMovie: (id: number, title: string) => void; }' is not assignable to type '{ movies: { id: number; title: string; }[]; }'.
Object literal may only specify known properties, and 'deleteMovie' does not exist in type '{ movies: { id: number; title: string; }[]; }'.ts(2322)
index.d.ts(337, 9): The expected type comes from property 'value' which is declared here on type 'IntrinsicAttributes & ProviderProps<{ movies: { id: number; title: string; }[]; }>'
From what I understand from the error, am I correct to say that I have yet to declare that my Provider's value did not include a 'function type'. If so, how can I amend this issue?
MovieContext.tsx
import React, { createContext, useReducer } from 'react';
import MovieReducer from '../reducers/MovieReducer';
const initialState = {
movies: [
{
id: 1,
title: "King Kong"
},
{
id: 2,
title: "Spiderman"
}
]
};
export const MovieContext = createContext(initialState);
export const MovieProvider = (props: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(MovieReducer, initialState);
// Actions
function deleteMovie(id: number): void {
dispatch({
type: 'DELETE_MOVIE',
payload: { id, title: 'nil' }
})
}
function addMovie(id: number, title: string): void {
dispatch({
type: 'ADD_MOVIE',
payload: { id, title }
})
}
return (
<MovieContext.Provider value={{
movies: state.movies,
deleteMovie,
addMovie
}}>
{props.children}
</MovieContext.Provider>
)
}
Do let me know if any part of the code can be improve as well! I just started working on Typescript recently and Context just literally today.
The type of the context is determined on it's declaration. You need to specify the type when calling createContext along with reasonable defaults if the context is retrieved without a context provider in the tree above:
// You could move the anonymous type between <> to an interface
export const MovieContext = createContext<{
movies: Movie[],
deleteMovie(id: number): void;
addMovie(id: number, title: string): void;
}>({
...initialState,
addMovie: () => {},
deleteMovie: () => {}
});
Playground Link

Resources