Optional property based on value of enum - reactjs

I'm trying to get some typings to work for a react useReducer.
Basically I have an action that has an optional property (data) based on the value of another property - so if STATUS is VIEW or EDIT, the action must have the data property. I almost have something working, but there's one case (see below) where this fails.
I guess one way of doing this is by explicitly setting STATUS.NEW to not require the extra property ({ type: 'SET_STATUS'; status: STATUS.NEW }), but I'm wondering if theres a better way. If in the future I added a bunch of different statuses then I'd have to specify each one to not require the data property.
Typescript Playground
enum STATUS {
NEW = 'new',
VIEW = 'view',
EDIT = 'edit'
}
/*
if status is 'view', or 'edit', action should also contain
a field called 'data'
*/
type Action =
| { type: 'SET_STATUS'; status: STATUS }
| { type: 'SET_STATUS'; status: STATUS.VIEW | STATUS.EDIT; data: string; }
// example actions
// CORRECT - is valid action
const a1: Action = { type: 'SET_STATUS', status: STATUS.NEW }
// CORRECT - is a valid action
const a2: Action = { type: 'SET_STATUS', status: STATUS.VIEW, data: 'foo' }
// FAILS - should throw an error because `data` property should be required
const a3: Action = { type: 'SET_STATUS', status: STATUS.EDIT }
// CORRECT - should throw error because data is not required if status is new
const a4: Action = { type: 'SET_STATUS', status: STATUS.NEW, data: 'foo' }
And the second part of the question is how I'd incorporate this into a useCallback below. I would have thought that useCallback would be able to correctly infer the arguments into the appropriate action type.
/*
assume:
const [state, dispatch] = useReducer(stateReducer, initialState)
*/
const setStatus = useCallback(
(payload: Omit<Action, 'type'>) => dispatch({ type: 'SET_STATUS', ...payload }),
[],
)
/*
complains about:
Argument of type '{ status: STATUS.EDIT; data: string; }' is not assignable to parameter of type 'Pick<Action, "status">'.
Object literal may only specify known properties, and 'data' does not exist in type 'Pick<Action, "status">'
*/
setStatus({ status: STATUS.EDIT, data: 'foo' })

You can define a union of statues that require data, then exclude them in action representing all the others:
enum STATUS {
NEW = 'new',
VIEW = 'view',
EDIT = 'edit'
}
type WithDataStatuses = STATUS.VIEW | STATUS.EDIT;
type Action =
| { type: 'SET_STATUS'; status: Exclude<STATUS, WithDataStatuses> }
| {
type: 'SET_STATUS';
status: WithDataStatuses;
data: string;
}
// now CORRECT - data is required
const a3: Action = { type: 'SET_STATUS', status: STATUS.EDIT }

Answer for second part of question :-)
Assuming that you have defined Actions as suggested by #Aleksey L., useCallback can by typed as follows
// This is overloaded function which can take data or not depending of status
interface Callback {
(payload: { status: Exclude<STATUS, WithDataStatuses> }): void;
(payload: { status: WithDataStatuses; data: string; } ): void;
}
const [state, dispatch] = React.useReducer(stateReducer, {})
// Explicitly type useCallback with Callback interface
const setStatus = React.useCallback<Callback>(
(payload) => dispatch({ type: 'SET_STATUS', ...payload }),
[],
)
setStatus({ status: STATUS.EDIT, data: 'foo' })
setStatus({ status: STATUS.NEW })
The working demo

Related

Argument of type Promise<MemberEntityVM[]> is not assignable to parameter of type SetStateAction<MemberEntityVM[]>

I want to show a list of members of Github, filtered by organization (for instance, members of Github which are employees of Microsoft). I am using React + TS. So, I have the following API Model (the interface of the JSON data that I receive after doing the call to the GitHub API):
export interface MemberEntityApi {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: string;
}
But I dont wanna use so much information, so I will create a ViewModel (just with the information I want to use). Here my ViewModel:
export interface MemberEntityVM {
id: number;
login: string;
avatar_url: string;
}
Here my call to the API:
export const getMembers = (organization: string, perpage, page): Promise<MemberEntityApi[]> => {
return fetch(`https://api.github.com/orgs/${organization}/members?per_page=${perpage}&page=${page}`)
.then((response) => {
if (response.status == 200) {
const data = response.json();
return data;
} else {
throw new Error(response.statusText)
}
})
.catch((e) => console.log(e));
}
Here my mapper function:
export const mapMemberEntityFromAPIModelToVM = (data: MemberEntityApi[]): MemberEntityVM[] => {
return data.map((data) => {
return {
id: data.id,
login: data.login,
avatar_url: data.avatar_url
}
})
}
So, in my component, the problem is the following one:
const [members, setMembers] = React.useState<MemberEntityVM[]>([]);
const [organization, setOrganization] = React.useState<string>("Microsoft");
useEffect(() => {
const membersVM = getMembers(organization, perpage, page)
.then((data) => mapMemberEntityFromAPIModelToVM(data));
console.log("membersVM", membersVM);
setMembers(membersVM);
console.log("members", members);
}, [])
This sentence console.log("membersVM", membersVM); is showing a response with an array of 3 members of a given organization (that is correct, since I just want to show 3 members per page) but this sentence console.log("members", members); shows an empty array. That means setMembers(membersVM); is not working. But I dont understand why. I am receiving the following mistake: "Argument of type Promise<MemberEntityVM[]> is not assignable to parameter of type SetStateAction<MemberEntityVM[]>" in the line I do setMembers(membersVM);. I dont understand this error message since membersVM has the following interface MemberEntityVM[] and the type of useState is the following one: React.useState<MemberEntityVM[]>. Where am I committing the mistake? Thanks in advance, kind regards.
Because getMembers(..).then(..) returns a promise of the mapped members, you will need to either await the value or deal with it in the then handler. Since useEffect does not allow an asynchronous function as its argument, the easiest solution is to call setMembers in the existing then handler:
useEffect(() => {
getMembers(organization, perpage, page)
.then((data) => {
const membersVM = mapMemberEntityFromAPIModelToVM(data);
console.log("membersVM", membersVM);
setMembers(membersVM);
console.log("members", members);
});
}, [])
If you do wish to use async/await, you can declare a local async function fetchMembers, and immediately call it:
useEffect(() => {
const fetchMembers = async () => {
const data = await getMembers(organization, perpage, page);
const membersVM = mapMemberEntityFromAPIModelToVM(data);
console.log('membersVM', membersVM);
setMembers(membersVM);
console.log('members', members);
}
fetchMembers();
}, [])

Property 'label' is missing in type 'string[]' but required in type - typescript error

I have a type called Person like this :
type Person = {
value?: string[];
label?: string[];
};
I have this promise function
async function main(): Promise<Person> {
const foo = await dynamicsWebApi.retrieveAll("accounts",["name"]).then(function (stuff) {
var records = stuff.value
const options = records?.map(d => ({
"value" : d.name,
"label" : d.name
}))
console.log(options)
}).catch(function (error) {
console.log(error)
})
return options
}
So I transform the api call into value and label (it is called options) and I need to pass this to a react component (in my return statement.
but I am getting the error
Type 'string[]' has no properties in common with type 'Person'
when I hover over the 'return options' command .
Any idea how I can craft my promise better to avoid this error ?
Thanks !
Well, it seems strange to me like you're retrieving a list of person objects, which each have 1 label and 1 value. This is not consistent with the Person object you defined.
If this is the case, this could be written like this:
type Person = {
value?: string;
label?: string;
}
async function main(): Promise<Person> {
const stuff = await dynamicsWebApi.retrieveAll("accounts",["name"]);
const records = stuff.value;
if (!records) {
return [];
}
const options: Person[] = records.map(d => ({
value: d.name,
label: d.name
}));
return options;
}

2d object whose key is the same as the keys of a value in the same object

I'm still a newbie in typescript and been searching in google about this for almost 2 hours now.
I want to create a type that is an object that will contain multiple values. The key of one value in the object would be the same as the keys of another value in that same object, except this time they will be functions.
Example value of this would be:
{
values: {
email: '',
password: ''
},
validators: {
email: () => { /* some function that does something and returns a string */ },
password: () => { /* some function that does something and returns a string */ },
}
};
In the example above, you see that the keys of validators are the same as the keys of values, except validators are composed of functions.
I can do:
export type Options = {
values: Record<string, unknown>,
validators: Record<string, unknown>
};
but this is not robust because I want the autocomplete to happen, I want validators to be a record which keys are the same as the keys of values, except all those are optional and should be functions.
I'm thinking of using generics, but I don't know what to pass to it
type FormValidator<T> = {
[P in keyof T]?: (values: Record<string, unknown>) => string;
};
export type FormOptions = {
values: Record<string, unknown>;
validators?: FormValidator<this.values>; // WHAT DO I PASS HERE SO THAT IT WILL GET THE KEYS OF VALUES?
};
EDIT 1:
One thing that I think works the same as I want this to work is this:
https://www.typescriptlang.org/play?#code/C4TwDgpgBAYg9gJwLYDUCGAbAlgEzcRAHgBUA+KAXigG8BYAKCigG0AFKLAOygGsIQ4AMyjEAugC4oACgCUlcgGdgCLgHMA3AwC+m+gwDGcTkqgA3TAFcICyjQZMmEJGiwZJAcncAaew7BoFBQB3RBwPd21dAyMTc2w8AgQbSXhkdHj8IlBIITNLa3IqOkYHSKA
Notice how the autocomplete works, and if you add something else that's not in values, it will also throw an error that's basically how I want it but I want it defined in that one type: type FormOptions
EDIT 2:
I'm wanting to do this in ReactJS / ReactNative and just plain NodeJS/TypeScript (that is, without using any frameworks)
Not sure if your question right but here's my example using type parameters
Sample snippet
// interface
interface FormParamConfigMap {
[paramName: string]: any;
}
// type parameter
const Form = <FormParamMap extends FormParamConfigMap>(options: {
values: FormParamMap;
filter: FormParamMap;
}): any => {};
///// usage
const formIntance = Form({
values:{
email:'',
password:''
},
filter:{
email:()=>'valid',
password: ()=>'valid'
}
})
It looks like you want compile time type inference (so auto-complete).
In that case we can't use this or typeof, etc.
And you want pre-defined key set (indexer) which should be shared by both values.
For this, I don't think there is any way other than string literals.
Such as:
type RecordKeys = 'email' | 'password';
type Options = {
values: Record<RecordKeys, string>,
validators: Record<RecordKeys, (values: RecordKeys) => string>
};
const o: Options = {
values: {
email: '',
password: ''
},
validators: {
email: () => { return 'OK'; },
password: () => { return 'OK'; },
}
};
This guide is very helpful: https://basarat.gitbook.io/typescript/type-system/index-signatures
I believe you have two options here:
Pass all keys as a generic parameter:
type Options<T extends string> = {
value: Record<T, string>,
validators: Partial<Record<T, () => string>>
}
type Keys = 'foo' | 'bar'
const options: Options<Keys> = {
value: {
foo: '',
bar: ''
},
validators: {
foo: () => 'foo',
bar: () => 'bar'
}
}
OR infer the type from function argument
const isValid = <Keys extends string>(options: Options<Keys>) => {}
isValid({
value: {
a: 'a',
},
validators: {
a: () => 'a'
}
}) // ok
isValid({
value: {
b: 'a', // error
},
validators: {
a: () => 'a'
}
}) // ok
Playground

How can I solve this error 'don't use object as a type'?

I do not understand this error message caused.
My component has two and one data array which has objects.
I got an error message 'Don't use object as a type. The object type is currently hard to use'.
How can i solve it ??
I attached the data which array has objects.
first.tsx...
import { dataList } from './xxx';
// This dataList ===
export const dataList = [
{
id: 1,
title: "text",
hidden1: "text",
},
{
id: 2,
title: "text",
hidden1: "text",
hidden2: "text",
},
{
id: 3,
title: "text",
hidden1: "text",
hidden2: "text",
hidden3: "text",
},
];
const First = () => {
return <Second data={dataList} />
}
second.tsx...
const Second = ({data} : { data:object[] } ) => {. << here, "Don't use 'object' as a type.
return <ul><li> {data.tag....blah blah} </li><ul>
}
error message :
Don't use `object` as a type. The `object` type is currently hard to use ([see this issue](https://github.com/microsoft/TypeScript/issues/21732)).
Consider using `Record<string, unknown>` instead, as it allows you to more easily inspect and use the keys #typescript-eslint/ban-types
You need to be explicit with the type of the data being destructured here.
You can just create an interface of the data.
Edit:
As your data has some optional parameters I updated the interface to reflect that.
interface IData {
id: number;
title: string;
hidden1: string;
hidden2?: string;
hidden3?: string;
}
Then replace object[] with IData[]. So now you specify that this takes in an array of the IData interface.
const Second = ({ data } : { data:IData[] }) => {
return <ul><li>{data}</li><ul>
}
I did not try this with React, so a bit different code style here, but Typescript compiles this even without creating an interface:
const second = (data: Record<string, unknown>[]) => {
return data
.map(item =>
Object.keys(item)
.map(key => `key:${key} = value:${item[key]}`)
.join(',')
)
.join(';');
};
I built in some mapping, but you could just return data too. Which of course would be useless, but it would compile.
This would compile too. Notice the array [] symbol in both solutions:
const second = (data: { [key: string]: unknown }[]) => {
return data
.map(item =>
Object.keys(item)
.map(key => `key:${key} = value:${item[key]}`)
.join(',')
)
.join(';');
};

React/TypeScript: Union Types in state of Context API

TypeScript does not seem to be recognizing that property state.recipes do exist when I use the state in some other component, this would be the case if YummlyState is the type of RecipesState. I suspect the YummlyState to always be the type of InitialState because that is the type it will have initially because of the initial state being set.
Also to include, is there anything else you notice about this Context which you think should be different?
Many thanks!
import React, {
createContext,
Dispatch,
PropsWithChildren,
ReactElement,
Reducer,
useContext,
useReducer,
} from 'react'
// Recipe
export type Recipe = {
id: number
title: string
image: string
readyInMinutes: number
diets: string[]
pricePerServing: number
servings: number
}
// Response
export type SpoonacularResponse = {
number: number
offset: number
results: Recipe[]
totalResults: number
}
// Yummly State
type StatusUnion = 'resolved' | 'rejected' | 'idle' | 'pending'
type InitialState = {
status: StatusUnion
}
type SingleRecipeState = InitialState & {
recipe: Recipe
}
type RecipesState = InitialState & {
recipes: Recipe[]
}
type ErrorState = InitialState & {
error: unknown
}
type YummlyState = InitialState | SingleRecipeState | RecipesState | ErrorState
// Action Union Type for the reducer
type Action =
| { type: 'pending' }
| { type: 'singleRecipeResolved'; payload: Recipe }
| { type: 'recipesResolved'; payload: Recipe[] }
// eslint-disable-next-line #typescript-eslint/no-explicit-any
| { type: 'rejected'; payload: unknown }
// The initial state
const initialState: YummlyState = {
status: 'idle',
}
// The Reducer
function yummlyReducer(state: YummlyState, action: Action): YummlyState {
switch (action.type) {
case 'pending':
return {
status: 'pending',
}
case 'singleRecipeResolved':
return {
...state,
status: 'resolved',
recipe: action.payload,
}
case 'recipesResolved':
return {
...state,
status: 'resolved',
recipes: action.payload,
}
case 'rejected':
return {
...state,
status: 'rejected',
error: action.payload,
}
default:
throw new Error('This should not happen :D')
}
}
type YummlyContextType = {
state: YummlyState
dispatch: Dispatch<Action>
}
const YummlyContext = createContext<YummlyContextType>({
state: initialState,
dispatch: () => {},
})
YummlyContext.displayName = 'YummlyContext'
// eslint-disable-next-line #typescript-eslint/ban-types
function YummlyProvider(props: PropsWithChildren<{}>): ReactElement {
const [state, dispatch] = useReducer<Reducer<YummlyState, Action>>(
yummlyReducer,
initialState
)
const value = { state, dispatch }
return <YummlyContext.Provider value={value} {...props} />
}
function useYummlyContext(): YummlyContextType {
const context = useContext(YummlyContext)
if (!context) {
throw new Error(`No provider for YummlyContext given`)
}
return context
}
export { YummlyProvider, useYummlyContext }
When dealing with a union, you will not be able to access a property such as state.recipes unless that property has been declared on ALL members of the union. There are essentially two ways that you can deal with this type of thing:
Check that the property key exists before trying to access it. If it exists, we know it is a valid value and not undefined.
Include a base interface in the YummlyState union which says that all of the keys of any of the members can be accessed, but their values might be undefined.
Guarding Properties
Without changing your type definitions, the simplest thing you can do is use a type guard to see if a property exists. Based on your union, typescript knows that if there is a property recipes, it must be of type Recipe[].
const Test = () => {
const {state, dispatch} = useContext(YummlyContext);
if ( 'recipes' in state ) {
// do something with recipes
const r: Recipe[] = state.recipes;
}
}
Declaring Optional Properties
The base interface that we want to include in our union looks like this:
interface YummlyBase {
status: StatusUnion;
recipe?: Recipe;
recipes?: Recipe[];
error?: unknown;
}
status is required, but all other properties are optional. This means that we can always access them, but they might be undefined. So you need to check that a particular value is not undefined before using it.
We are able to destructure the object, which is nice:
const base: YummlyBase = { status: 'idle' };
const {status, recipe, recipes, error} = base;
Using just the YummlyBase alone is ok, but it doesn't give us all of the information. It's better if YummlyState is the base and a union of specific members.
type YummlyState = YummlyBase & (InitialState | SingleRecipeState | RecipesState | ErrorState)
Discriminating Unions
Each of your scenarios has a different string literal for status (well, mostly), but we haven't made use of that fact. Discriminating unions is a way to narrow the type of the object based on the value of a string property like status.
You are already doing this with your Action union type. When you switch based on action.type, it knows the correct type for action.payload.
This can be extremely helpful in some situations. It is less helpful here because the status resolved is used by both SingleRecipeState and RecipesState, so you still need additional checking. That's why I've put this option last.
type InitialState = {
status: 'idle' | 'pending';
}
type SingleRecipeState = {
status: 'resolved';
recipe: Recipe
}
type RecipesState = {
status: 'resolved';
recipes: Recipe[];
}
type ErrorState = {
status: 'rejected';
error: unknown;
}
type YummlyState = InitialState | SingleRecipeState | RecipesState | ErrorState
type StatusUnion = YummlyState['status'];
const check = (state: YummlyState) => {
if (state.status === 'rejected') {
// state is ErrorState
state.error;
}
if ( state.status === 'resolved' ) {
// state is RecipesState or SingleRecipeState
state.recipe; // still an error because we don't know if it's single or multiple
}
}
Playground Link

Resources