Object key optional with typescript — but being able to call it - reactjs

I have the following interfaces declared
export interface User {
...
first: string;
...
}
export interface UserDataState {
credentials: User | {};
..
}
In my react component, I want to be able to access the first in the UserDataState like this:
const userData = useSelector((state: RootState) => state.user.credentials);
Welcome, { userData.first }
However I'm getting this error
Property 'first' does not exist on type '{} | User'.
So I tried the following:
let userFirst = '';
if (userData.hasOwnProperty('first')) {
userFirst = userData.first;
}
...
return ( <span>{userFirst}</span>)
and..
return ( {userData && userData.first && (<span>{userData.first}</span>)})
I don't want to make the user first optional and I need to be able to return an empty object to as the credentials. This is very much annoying.

I suggest that you use optional chaning.
For example, in your case, I would do the following:
export interface UserDataState {
credentials: User | undefined;
}
To access first from crendentials from state (suppose state is nullable), instead of doing:
state && state.credentials && state.credentials.first
You just need to use the optional chaining operator ?.
state?.credentials?.first
By doing this you won't compromise the type safety of your codebase with explicit casting, for example casting to any.
This idiom is pretty standard with Typescript 3.7 and above.

I got it working with
const user = useSelector((state: RootState) => state.user.credentials);
const userData: any = user;
// or this works to
import { User as UserType } from '../types';
const userData: { first?: UserType['first'] } = user;
Not sure if this is best practice though, if anyone has a better method would love to hear it

You can do something like this, so basically what you are doing is enforcing the type that it is going to be a User type:
const userData = useSelector((state: RootState) => state.user.credentials) as User;

Related

How to type check payload provided to action: React Typescript (React hooks)

I have created a global context that will manage several of the UI states of my application. Currently the state just have only one property that will handle whether to show a Login view or a Signup view.
I have also typed the State and Action as following:
interface State {
authView: AUTH_VIEWS
}
const initialState: State = {
authView: 'LOGIN_VIEW',
}
type Action = {
type: 'SET_AUTH_VIEW'
view: AUTH_VIEWS
}
type AUTH_VIEWS = 'LOGIN_VIEW' | 'SIGNUP_VIEW'
Here is the action creator:
const setAuthView = (view: AUTH_VIEWS) => dispatch({ type: 'SET_AUTH_VIEW', view })
Since I have explicitly typed the type of views that should be accepted when passed as a payload to action, I have expected that there should be a compile time error or warning (from Typescript) in case wrong payload is passed or there is some typo, but that wasn't the case.
e.g. Show some error when following happens:
setAuthView('SIGNVIEW') //typo
or
setAuthView('SOME_WRONG_PAYLOAD')
So the question is, how to correctly use Typescript to restrict the payload to correct type.
Edit:
Here is the code sandbox link for the complete code.
https://codesandbox.io/s/typescript-context-c05d7
Following the discussion with Drag13, I suspect that the setAuthView is not correctly exported and that might be the reason for faulty typing when importing it in other component.
An issue is in this line of code:
export const UIContext = createContext<State | any>(initialState)
Basically you are saying that type of your context can be State OR any which, at the end, means that your context can be any. Take a look into this simple example:
const sum = (a:number, b: number | any) => a+b;
sum(1,1); // works
sum(1, 'i am not a number!!!'); // still works because b can be any :(
First example works as expected, but the second also works, because b can be anything!
To fix your issue you should return any from declarations:
export const UIContext = createContext<State>(initialState)
The simplest fix will be looks like this:
changing State type to support setAuthView function
interface State {
authView: AUTH_VIEWS;
setAuthView: (e: AUTH_VIEWS) => void;
}
Providing dummy realization for setAuthView to make TS happy (a bit ugly, yes)
const initialState: State = {
authView: "LOGIN_VIEW",
setAuthView: () => void 0,
};
Now it should work and check your types correctly
I would also recommend to read about the generics this will helps you pretty much.
I think the other answer explains your mistake quite well, but since you were not sure how to structure a Context with useReducer and TypeScript, here is how I do it. Feel free to copy-paste this and fit it to your needs.
import { createContext, Dispatch, FC, useReducer } from "react";
// how is the state structured?
interface DemoContextState {
selectedArtNr: string;
featureEnabled: boolean;
externallySelected: boolean;
}
// actions are a discriminated union. They all have a 'type' but can have differing payloads or none at all
type DemoContextActions = {
type: "SET_SELECTED_ARTNR";
payload: {
artnr: string;
externallySelected?: boolean;
};
} | {
type: "TOGGLE_FEATURE";
};
const initialState: DemoContextState = {
selectedArtNr: "",
featureEnabled: false,
externallySelected: false,
};
// our context will be made up of state and dispatch.
export const DemoContext = createContext<{state: DemoContextState, dispatch: Dispatch<DemoContextActions>}>({state: initialState, dispatch: ()=>{}});
// our reducer. Here we define all state transitions
const demoContextReducer = (state: DemoContextState, action: DemoContextActions): DemoContextState => {
switch(action.type){
case "SET_SELECTED_ARTNR": {
const selectedArtNr = action.payload.artnr;
const externallySelected: boolean = action.payload.externallySelected || false;
return {...state, selectedArtNr, externallySelected};
}
case "TOGGLE_FEATURE":
return {...state, featureEnabled: !state.featureEnabled};
default:
// TS will throw a compile time error if you've defined actions that are not handled
const exhaustiveCheck: never = action;
throw new Error(`Unhandled action: ${action}`);
}
};
// use this context provider high up in the component tree
export const DemoContextProvider: FC = ({children})=>{
const [state, dispatch] = useReducer(demoContextReducer, {...initialState});
return (
<DemoContext.Provider value={{state, dispatch}}>
{children}
</DemoContext.Provider>
);
};
As you can see, I pass an object with two variables in my context: state and dispatch.
You can use it like so:
// in component
const {state, dispatch} = useContext(DemoContext);
// do whatever you want with state and dispatch. If you just need to show the current state, just destructure state. If you just need to mutate the state, just destructure dispatch
So Drag13 pointed me to the right direction that the problem is with the type given to Context. Then after a lot of further researching and brainstorming I came up with this solution.
interface ContextProps {
state: State
dispatch: Dispatch<Action>
}
export const UIContext = createContext<ContextProps | undefined>(undefined)
The custom hook below does the undefined check
export const useUIContext = () => {
const context = useContext(UIContext)
if (context === undefined) {
throw new Error(`useUI must be used within a UIProvider`)
}
return context
}
export const UIProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(UIReducer, initialState)
return <UIContext.Provider value={{ state, dispatch }}>{children}</UIContext.Provider>
}
Reading this blog https://kentcdodds.com/blog/how-to-use-react-context-effectively made me to ditch the action creator pattern and directly exposing the dispatch method for state mutation as with typescript and strongly typed Actions there is no need for the unnecessary abstraction of using the action creators.

React Redux Typescript - how to set types?

I'm having hard time setting the types for this function:
interface fetchedCountries {
mergedArray?: [];
}
export const fetchCountries = () => {
return (dispatch:(() => void)) => {
console.log(dispatch)
fetch(countryListJsonFile)
.then((response) => response.json())
.then((jsonData: fetchedCountries) => {
const array = Object.entries(jsonData)[0];
const countries = array[1].map((el: any) => {
return el._id;
}).sort();
dispatch(setCountries(countries));
})
.catch(function (err) {
console.error(err);
});
};
};
it says that setCountries expected 0 arguments, but got 1.
I've tried to follow different guides, such as this, but I couldn't make it work.
I got lost. I want here to get rid of all the any types, and give the real ones.
The store has exported:
export type RootState = ReturnType<typeof store.getState>;
export type AppState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;
Here is the slice:
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
countryList: [],
};
const slice = createSlice({
name: 'countryList',
initialState,
reducers: {
setCountries(state, action) {
state.countryList = action.payload;
},
},
});
export const { setCountries } = slice.actions;
export default slice.reducer;
Can anyone please help?
There are a few issues here in your code are all related. You need to properly define:
The type of dispatch in your fetchCountries function.
The type of the payload for setCountries.
The type of your state.
The type of your API response.
Incorrect or missing types higher up in the chain can cause issues further down. For example, when you find yourself setting a type in a .map() callback like this:
array[1].map((el: any) => {
It means that the array itself (array[1]) has the wrong type. So let's figure out where it's going wrong.
1. Dispatch Type
#T.D. Stoneheart is correct. The expected 0 arguments, but got 1 error comes from calling dispatch(...), not from calling setCountries(...).
Your definition dispatch:(() => void) says that dispatch does not take any arguments. This is obviously incorrect.
The good news is that you already have the correct type elsewhere in your code. It is the AppDispatch which you exported from your store file.
export const fetchCountries = () => {
return (dispatch: AppDispatch) => {
This fix is enough to resolve all red-underlined errors. But there are some other mistakes and omissions that you may want to fix.
2. Payload Type
If you don't explicitly set the type for setCountries(state, action), then your payload type becomes any. This is fine, but not ideal. It says that "anything goes" which can make it hard to see genuine problems.
To assign the correct type to the action, import the PayloadAction utility type for redux toolkit:
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
And use it with the type of your payload, which is an array of countries. Looking at your thunk, these seem to be string?:
reducers: {
setCountries(state, action: PayloadAction<string[]>) {
state.countryList = action.payload;
},
},
3. State Type
Remember how I said that any can hide genuine problems? If you followed step 2 then you should be seeing one of those now.
The assignment of state.countryList = action.payload; is giving an error:
Type 'string[]' is not assignable to type 'never[]'.
Your state.countryList has type never[] because it had an initial value of [] and that's all that TypeScript knows. It doesn't know that this is supposed to be an array of country ids. You can fix that by assigning a more accurate type to your `initialState.
Either do this:
const initialState = {
countryList: [] as string[],
};
Or this:
interface SliceState { // can name it whatever
countryList: string[];
}
const initialState: SliceState = {
countryList: [],
};
Having a correct type here will make it much, much easier to use data that you've selected from your state because now RootState has the correct type for the countryList property.
4. API Response Type
interface fetchedCountries {
mergedArray?: [];
}
This type is saying that the JSON from your response is an object which maybe has a property mergedArray which is an empty array. That's it.
I'm not sure what the actual data looks like, but perhaps something like this?
interface Country {
_id: string;
}
interface FetchedCountries {
mergedArray: Country[];
}
So now you don't need to use (el: any) because TypeScript already knows that el is a Country object.
.then((jsonData: FetchedCountries) => {
const countries = jsonData.mergedArray
.map(el => el._id)
.sort();
dispatch(setCountries(countries));
})
TypeScript Playground Link
Actually, the problem lies in the parameter type of the returning function.
You returned (dispatch: ((/* no parameters */) => void)) => {} but you called dispatch(setCountries(countries)) which has one argument setCountries(countries). Setting correct number of parameters will fix, like return (dispatch: ((something: unknown) => void)) => {}.

Why are these tuple types conjoined?

I have recreated the error I'm getting in a brandnew Create-React-App repo, source code can be found here.
As you can see in MyContext.tsx, I'm simply adding the result of useState to my context. The type of the context is also defined as a tuple by
const UDContext = createContext<
| undefined
| [
UserData | undefined,
React.Dispatch<React.SetStateAction<UserData | undefined>>
]
>(undefined);
However, when I try to access the data in MyComponent.tsx by doing
const [userData] = useUserData();
I'm getting the error
Property 'username' does not exist on type 'UserData | Dispatch<SetStateAction<UserData | undefined>>'.
This to me looks like it is somehow combining the tuple's types to one?
The funny thing: the below code does actually work (it does give a warning that context may be undefined, which indeed makes sense).
const context = useUserData();
const userData = context[0];
Exactly the same is happening in MySetter.tsx, where the exact error is
Not all constituents of type 'UserData | Dispatch<SetStateAction<UserData | undefined>>' are callable.
The above "solution" without direct destructuring works here as well.
The only way I can get this to work, is by explicitly declaring the type of the return value of useUserData.
A few sidenotes:
This is the #next version of Create-React-App
I had to enable --downlevelIteration, or else the destructuring of useUserData() was giving Type '[UserData | undefined, Dispatch<SetStateAction<UserData | undefined>>] | undefined' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.
I have a working example in CodeSandbox that gives no issues at all.
This seems to be a BUG in typescript and I found two open bugs with similar issues BUG 31613 and BUG 40215
Apart from the option you mentioned in question (explicit type on return), another option would be to not use tuple destructuring and for your code it would look like:
MyComponent.tsx
import React from "react";
import { useUserData } from "./MyContext";
const MyComponent = () => {
const userData = useUserData();
if (!userData) {
return <div>Loading username</div>;
}
return <div>{userData[0]?.username}</div>;
};
export default MyComponent;
MySetter.tsx
import { useEffect } from "react";
import { useUserData } from "./MyContext";
const MySetter = () => {
const userData = useUserData();
// Fakes an API call
useEffect(() => {
setTimeout(() => {
userData && userData[1]({ username: "FooBar" });
}, 750);
});
return null;
};
export default MySetter;
That's because your context can be also undefined so you need to make checks to make sure you always return something from your context. You could create reusable method for creating context thats always something, and not undefined:
function createCtx<T>() {
const Context = React.createContext<T | undefined>(undefined);
const useCtx = () => {
const ctx = React.useContext(Context);
if (typeof ctx === "undefined") {
throw new Error("useContext must be inside provider with a value.");
}
return ctx;
};
return [useCtx, Context.Provider] as const;
};
Then use this method as such where you'd probably export useUserData and UserDataContext
const [useUserData, UserDataProvider] = createCtx<[
UserData | undefined,
React.Dispatch<React.SetStateAction<UserData | undefined>>
]>();
const UserDataContext: React.FC = ({ children }) => {
const [userData, setUserData] = React.useState<UserData>();
return (
<UserDataProvider value={[userData, setUserData]}>
{children}
</UserDataProvider>
);
};
and use it as such in your components
export default function App() {
const [userData] = useUserData();
return (
<h2>{userData?.username}</h2>
);
}
Not only does this solve your problem, but also gives you reusable method that helps you with other things that you'll surely need.
Cheers.

TypeScript undefined object type messes things up

I am often running into a situation where I store an object in React state, such as a User:
...
user: undefined,
setUser: (user) => set((state) => ({ user: { ...user, ...state.user } as User })),
I define the TypeScript type as
export type User = {
verified: boolean;
firstName: string;
lastName: string;
userType: 'user' | 'admin';
};
So, at the moment TypeScript will complain that user, which I instantiate as a User type is undefined, while it cannot be. FINE! So I add | undefined at the end of the User type definition. So that will take care of this specific file, but then I start getting a million and one other warnings all over, because, for example, I call user.firstName, but now that the user can be undefined, I really should call user?.firstName.
Adding conditions, such as if(user) does not seem to satisfy TS. So I end up spending three hours fixing stuff for TS, only to find out that my code now breaks because of this recent change.
It feels very much like trying to put down fires that I started myself when I used TypeScript. Frustrating.
Can someone who know TS well please offer some guidance? How would you approach this? Is there a way to handle such "undefined" cases?
Assuming you have defined user as:
const user: User|undefined = ...;
If you want to access a property of user, use this syntax:
user!.firstName
This tells typescript that you know what you're doing and that user is never undefined or null. It also tells typescript that properties accessed this way are not to be assumed to possibly be null or undefined, which I'm guessing is why you were now getting more errors.
Note!
const user: User|undefined = ...;
is redundant typing. You can just revert back to using
const user: User = ...;
but instead of using the ?. operator to access properties, continue using !..
Sorry if this is your first time using typescript. It's usually not this bad.
Consider splitting your components into:
Components that MUST have a user
Components that MAY have a user
From the component that may have a user, render the component that must have one.
class UserComponent extends React.Component<{ user: User }> {
render() {
// Note: since user must be defined for this component, we are able to access properties without the ? or ! syntax
return (
<div>{this.props.user.firstName}</div>
)
}
}
// Note: this.state.user is optional, and therefore is initialized as undefined
class MayHaveUserComponent extends React.Component<{}, { user?: User }> {
render() {
if (this.state.user) {
return <UserComponent user={this.state.user} />
}
return (
<div>No user yet!</div>
)
}
setUser(user: User) {
this.setState({ user: { ...user, ...this.state.user } })
}
}
You're not just making typescript happy here. If you pass user: undefined to code that expects there to be a user object, then that will definitely cause runtime errors when you use undefined like an object. These checks are necessary if that is to be allowed.
Typically, I handle that in react by returning a render early if a required value is null or undefined. After that return, you can assume the user exists as normal.
class Component({ user: User | null | undefined }) {
if (!user) return <SomeLoadingIndicator /> // or: return null, or whatever
// from here on, `user` is known to be of type `User`, not null, and not undefined.
return <div>{user.lastName}</div>
}
Adding conditions, such as if(user) does not seem to satisfy TS.
It really does though. You just have to be on a conditional branch where the check has been run. Meaning you have to be in the { } of an if statement:
const user: User | null = Math.random() > 0.5 ? getUser() : null
if (user) {
// here user is of type: User
} else {
// here user is of type: null
}
// here user is of type: User | null
Or in code that could only run if the conditional was truthy.
const user: User | null = Math.random() > 0.5 ? getUser() : null
if (!user) {
// here user is of type: null
throw new Error("user is required!")
}
// here user is of type: User

Property does not exist on type within Redux reducer using TypeScript?

I'm trying to use Redux with TypeScript in a little learning project, following this tutorial:
https://redux.js.org/recipes/usage-with-typescript/
const INITIAL_STATE: TweetsState = {
tweets: []
};
const tweetsReducer: Reducer<TweetsState> = (
state = INITIAL_STATE,
action: TweetAction
): TweetsState => {
switch (action.type) {
case GET_TWEETS:
return {
...state
};
case TWEETS_RECEIVED:
return {
tweets: [...state.tweets, action.payload]
};
default:
return state;
}
};
My reducer expects an action of type TweetAction, which is the union of two interfaces:
export interface IGetTweetsAction {
type: typeof GET_TWEETS;
}
export interface ITweetsReceivedAction {
type: typeof TWEETS_RECEIVED;
payload: ITweet[];
}
export const GET_TWEETS: string = "GET_TWEETS";
export const TWEETS_RECEIVED: string = "TWEETS_RECEIVED";
export type TweetAction = IGetTweetsAction | ITweetsReceivedAction;
However, within my reducer, I get the error Property 'payload' does not exist on type 'TweetAction' which is confusing because it suggests the union hasn't worked?
But this is further confusing because within my actions file it clearly allows me to return an object, of type TweetAction, with a payload:
import {
TweetAction,
GET_TWEETS,
TWEETS_RECEIVED,
ITweet
} from "./tweets.types";
export const getTweets = (): TweetAction => ({
type: GET_TWEETS
});
export const tweetsReceived = (tweets: ITweet[]): TweetAction => ({
type: TWEETS_RECEIVED,
payload: tweets
});
I'm aware the code is currently a bit of a mess (I sometimes instinctively prefix interfaces with I being a C# dev, sorry!) as I haven't been through a refactory stage yet as I'm just trying to whip something up quickly, but I've been stumped by this error.
Can anyone see what's going wrong? It's probably something straight forward but the isolation might be getting to my head.
Thanks!
Edit: Note that removing the typesafety and changing:
action: TweetAction to just action allows me to do action.payload and the functionality works fine...
-- edit -----
Ok So apparently you need to remove the : string typing in those lines:
export const GET_TWEETS: string = "GET_TWEETS";
export const TWEETS_RECEIVED: string = "TWEETS_RECEIVED";
-- /edit -----
I'm pretty new to typescript too, but as I understand it, since you use action.payload in your reducer, the type TweetAction is not accepted since it could imply a IGetTweetsAction type, which does not define a payload property.
You need to use a more generic type like:
type TweetAction {
type: // ...,
}
to say that you can have an action that will always have a type and might have some other props (such as a payload).
As for your confusing example, I think that actually makes sense:
export const getTweets = (): TweetAction => ({
type: GET_TWEETS
});
No payload is defined so it matches IGetTweetsAction.
export const tweetsReceived = (tweets: ITweet[]): TweetAction => ({
type: TWEETS_RECEIVED,
payload: tweets
});
While this one matches ITweetsReceivedAction.

Resources