Why is encumbranceByType object possible undefined? - reactjs

I have a react component that takes in data of type EncumbranceData[] and restructures that data before mapping through it. This restructuring involves creating an empty object of type EncumbrancesByType and creating keys and values for it based on encumbranceData
However, I keep getting an "Object possible undefined" error where it says encumbrancesByType[e.type].push(e.suite). I don't understand how this could be undefined, as none of the fields on these types are optional. This component will always receive data of type EncumbranceData that will always have a type, id, and suite. EncumbranceData cannot be undefined. The object starts as an empty one, but each iteration through the forEach method should initialize each key with a truthy empty array.
How can the object possible be undefined, then, at the push(e.suite) statement?
I can get it to work by just using a bunch of optional operators (encumbrancesByType?.push()), but I feel like that defeats the point of type safety.
type EncumbranceData = {
id: string;
suite: string;
type: string;
};
interface EncumbrancesByType {
[key: string]: string[];
}
type EncumbranceAlertByTypeProps = {
id: string;
suites: string[];
type: string;
};
const EncumbranceAlertByType: React.FC<EncumbranceAlertByTypeProps> = ({ id, suites, type }) => {
const renderSuites = suites.map((s) => <span>{s}</span>);
return (
<div>
<div>{type}</div>
{renderSuites}
</div>
);
};
type ConflictingEncumbrancesAlertProps = {
encumbranceData: EncumbranceData[];
isOpen: boolean;
onContinue(): void;
onCancel(): void;
suiteIdsToCheck: string[];
encumbranceTypesToConsider?: EncumbranceType[];
};
const ConflictingEncumbrancesAlert: React.FC<ConflictingEncumbrancesAlertProps> = ({
encumbranceData,
isOpen,
onContinue,
onCancel,
suiteIdsToCheck,
encumbranceTypesToConsider,
}) => {
const encumbrancesByType: EncumbrancesByType = {}
encumbranceData.forEach((e) => {
if (!encumbrancesByType[e.type]) encumbrancesByType[e.type] = [e.suite]
else encumbrancesByType[e.type].push(e.suite)
})
const encumbrancesContent = Object.keys(encumbrancesByType).map((type) => (
<EncumbranceAlertByType suites={encumbrancesByType[type]} type={type} />
));
return <div>{encumbrancesContent}</div>;
};
export default ConflictingEncumbrancesAlert;

You likely have the noUncheckedIndexedAccess rule enable in your tsconfig. When you have this rule the compiler will always complain on unchecked index access.
Also, TS won't narrow down (remove the undefined) on index access. In order to have the compiler do that, you'll have to use an intermidiate variable.
encumbranceData.forEach((e) => {
const foo = encumbrancesByType[e.type]; // intermediate variable
if (foo) {
foo.push(e.suite); // ok
}
});
Playgroud

Related

React with TypeScript: How to type a Component in a Library? (Part 1)

In a codebase I am working on we are extracting components which should work in a generic way (because we want to put them into a separate npm package eventually).
Running Example
For example, we have the following Form component in React which uses any at two places. It should somehow behave in a generic way here (or derive the types from the given props):
// THIS SHOULD BE LIKE A FORM THAT WE CANNOT TOUCH
// BECAUSE IT COMES FROM A LIBRARY
// SO HOW TO TYPE IT PROPERLY?
import * as React from "react";
type FORM_ELEMENT_TYPE = "textfield" | "numberfield";
export type FormElement = {
key: string;
label: string;
type: FORM_ELEMENT_TYPE;
};
type FormProps = {
formData?: any; // <- HOW TO TYPE IF formData CAN BE ANYTHING? PERHAPS WITH THE HELP OF formElements?
formElements?: FormElement[];
onChange: (form: any) => void; // <- HOW TO TYPE IF WE DO NOT KNOW THE FORM? WE KNOW THE formElements TO DERIVE THE OUTPUT FROM, NO?
};
const Form: React.FC<FormProps> = ({
formData = {},
formElements = [],
onChange,
}) => {
return (
<form>
{formElements.map((formElement) => {
if (formElement.type === "textfield") {
return (
<label key={formElement.key}>
{formElement.label}:
<input
type="text"
value={formData[formElement.key]}
onChange={(event) =>
onChange({
...formData,
[formElement.key]: event.target.value,
})
}
/>
</label>
);
}
if (formElement.type === "numberfield") {
return (
<label key={formElement.key}>
{formElement.label}:
<input
type="number"
value={formData[formElement.key]}
onChange={(event) =>
onChange({
...formData,
[formElement.key]: event.target.valueAsNumber,
})
}
/>
</label>
);
}
return null;
})}
</form>
);
};
export default Form;
The App component -- where we have control, because it's not a component in the library -- uses the Form component. Here we have the issue of not knowing about the type in the onChange event handler which our Form component uses:
import * as React from "react";
import Form, { FormElement } from "./Form";
const App = () => {
const [formData, setFormData] = React.useState({
name: "David",
age: 20,
});
const formElements: FormElement[] = [
{
key: "name",
label: "Name",
type: "textfield",
},
{
key: "age",
label: "Age",
type: "numberfield",
},
];
// IS THERE ANY WAY TO INFER THIS? v
const handleChange = (newFormData: any) => {
setFormData(newFormData);
};
return (
<Form
formData={formData}
formElements={formElements}
onChange={handleChange}
/>
);
};
export default App;
How can I type the Form and App components with generics or/and inference to always know about the correct types for all 3 places where any is used at the moment? For the sake of completeness, here a link to the complete code as a running example.
I'd like to hear readers' opinions on Part 2 where the Form component gets a new prop called customFormElements.
It is a bit tricky. First I'll not concern myself with the implementation of Form and not care about type errors inside and just try to find a way to type Form so that when you use it from outside, the typings are correct.
There are two main ways to go when putting a generic here: you could either try to infer the value for formData given the value of formElements, or you could use the descriptions for fields from formElements and use them to infer the type of formData. The latter option probably makes more sense, so let's start with this one:
// I'll need this type later so I move it to enum
enum FormElementType {
// I believe adding "field" suffix here is redundant, but you may return
// it if you'd like
text = 'text',
number = 'number',
}
// What values must be passed for field of each type
type FieldTypeMap = {
[FormElementType.text]: string,
[FormElementType.number]: number,
}
export type FormElement = {
key: string;
label: string;
type: FormElementType;
};
// See explanation below
type FormProps<TElement extends FormElement> = {
data: FormData<NoInfer<TElement>>;
onChange: (form: FormData<NoInfer<TElement>>) => void;
elements: readonly TElement[];
};
// Utility types
type NoInfer<T> = T extends any ? T : T;
type FormData<TElement extends FormElement> = {
[Key in TElement["key"]]:
TElement extends infer U extends FormElement & {key: Key}
? FieldTypeMap[U['type']]
: never
}
export declare const Form: <TElement extends FormElement>({
data,
elements,
onChange,
}: FormProps<TElement>) => JSX.Element;
So the generic argument acccepts a union of objects that describe form fields, like in your example
TElement = {
key: "name",
label: String,
type: FormElementType.text,
} | {
key: "age",
label: "Age",
type: FormElementType.number,
}
Then we use FormData type to extract the shape for formData object from this union. TElement extends infer U extends FormElement & {key: Key} will map over values of TElement union, find the one that has its key property equal to Key and assign it to U. Then we extract the value type for this field using U['type'] field and a predefined mapping between value types and FormElementType values. Note: you need at least TS 4.8 for infer ... extends ... syntax to work.
The NoInfer thing is a trick that basically tells typescript that it must not infer the value for TElement generic argument from the places where it is wrapped into NoInfer. So when using Form typescript will only be allowed to infer TElement from the value of the formElements field, and not from formData.
Also I made formData and onChange required arguments, because I'm not sure what it means for them to be optional.
This approach works, but it's a bit awkward to use, because by default when defining an object with string values TS doesn't infer them as string literals but just as string values. Therefore you will have to add as const to formElements
const formElements = [
{
key: "name", // because of "as const" this is inferred as `"name"` and not as just `string`
label: "Name",
type: FormElementType.string,
},
{
key: "age", // Similarly this is inferred to be `"age"` and not just `string`
label: "Age",
type: FormElementType.number,
}
] as const
<Form
elements={formElements}
// ...
/>
When TS 5 is out, this might become less awkward, because you will be able to write TElement extends const FormElement or something.
Going the other way is also possible
type FormProps<TData extends {}> = {
data: TData;
onChange: (form: TData) => void;
elements: FormElements<TData>;
};
type ElementType<T> =
// You neeed to manually check for all possible `T` types here and infer what the values for `type` in `formElements` might be
T extends string ? FormElementType.text :
T extends number ? FormElementType.number : never
type FormElements<TData extends {}> = ReadonlyArray<{
[key in keyof TData]: {
key: key,
label: string,
type: ElementType<TData[key]>
}
}[keyof TData]>
export declare const Form: <TData extends {} extends {}>({
data,
elements,
onChange,
}: FormProps<TData>) => JSX.Element;
This way you don't need to use as const but only when passing formElements to Form inline:
<Form
elements={[
{
key: "name",
label: "Name",
type: FormElementType.string,
},
{
key: "age",
label: "Age",
type: FormElementType.number,
}
]}
// ...
/>
And this is also very awkward, because if you use wrong types you will get an error on formElements and not on formData field which is confusing and weird.
One way how to get rid of having to use this as const I can think of is the following: if you can change your API so that you use an object of formElements and not an array, then we can make typescript infer key names from this object keys. Moreover if instead of FormElementType being an enum, you make it an object with literal properties, you can use its values to better infer the value of the type field. Something like this:
export type FormElementType =
(typeof FormElementType)[keyof typeof FormElementType];
export const FormElementType = {
text: 'text',
number: 'number',
} as const;
type FieldTypeMap = {
[FormElementType.text]: string;
[FormElementType.number]: number;
};
export type FormElement = {
label: string;
type: FormElementType;
};
// See explanation below
type FormProps<TElements extends Record<string, FormElement>> = {
data: FormData<NoInfer<TElements>>;
onChange: (form: FormData<NoInfer<TElements>>) => void;
elements: TElements;
};
// Utility types
type NoInfer<T> = T extends any ? T : T;
type FormData<TElements extends Record<string, FormElement>> = {
[Key in keyof TElements]: FieldTypeMap[TElements[Key]["type"]];
};
export declare const Form: <TElements extends Record<string, FormElement>>({
data,
elements,
onChange,
}: FormProps<TElements>) => JSX.Element;
And use it like this:
const formElements = {
name: {
label: "Name",
type: FormElementType.text,
},
age: {
label: "Age",
type: FormElementType.number,
},
}
<Form
data={formData}
elements={formElements}
onChange={handleChange}
/>
Now speaking about the implementation of Form, I don't think you can get away and not see any errors inside Form if you use such complicated types, so honestly good luck typing it. I think it's mostly just easier to use type casts, or to not type it very meticulously at all, and just move these complex typings to a separate declaration file, where they will be visible from outside, and not interfere with implementation in your npm package.
I didn't write a full working implementation of this form, but here were some ideas you may find useful to type this mess. Forms are hard

Why does TypeScript not throw an error when a variable with wrong type is passed to useState?

Why does the TypeScript not throw an error when a variable is passed to useState? Is there a way I can still type check when passing a variable?
type BuildingNames = [
'farm',
'sawmill',
];
type BuildingType = { [n in BuildingNames[number]]?: number };
interface ITownContext {
buildings: BuildingType;
setBuildings: (buildings: BuildingType) => void;
}
interface TownProviderProps {
children: ReactNode;
}
const defaultValues = {
resourceBuildings: {
farm: 1,
asdfasdf: 5,
},
};
const TownContext = createContext<ITownContext>({} as ITownContext);
const TownProvider = ({ children }: TownProviderProps) => {
// no error
const [buildings, setBuildings] =
useState<BuildingType>(defaultValues.buildings);
// error occurs
const [buildingsDirect, setBuildingsDirect] =
useState<BuildingType>({ asdf: 1 });
return (
<TownContext.Provider
value={{
buildings,
setBuildings,
}}
>
{children}
</TownContext.Provider>
);
};
export { TownContext, TownProvider };
The difference you see between the 2 usages of useState is the effect of TypeScript excess property check, which triggers only when doing an assignment of an object literal (i.e. as opposed to assigning the reference of another variable, like defaultValues.buildings).
The fact that excess property check is not enforced when assigning the reference of another variable, enables a classic OOP usage of passing a subtype (here an object with more properties than strictly necessary).
Still, even when excess property check does not kick in, TS still checks for other errors: should one of the properties not match its type, it will be reported.
const defaultValues = {
buildings: {
farm: "string instead of number",
asdfasdf: 5,
},
};
useState<BuildingType>(defaultValues.buildings) // Types of property 'farm' are incompatible. Type 'string' is not assignable to type 'number'.
// ~~~~~~~~~~~~~~~~~~~~~~~
Playground Link

How to use TypeScript union types in react

I am trying to apply types to ReactJS props as implemented in the following interface. AS you can see the type tags is union type
export interface TagInterface {
id: number;
name: string;
}
export interface PostInterface {
tags: TagInterface[] | string[];
}
I have been able to to successfully use this type in a component inside a function as shown
onClick={() => handleClick(post.tags[0] as string)}
Now when i try to implement this type on another component TypeScript is throwing an error
Property 'name' does not exist on type 'string | TagInterface'.
Property 'name' does not exist on type 'string'.
useEffect(() => {
const getPostData = async () => {
const { data: posts }: DataInterface = await getRelatedPosts(
post.tags[0].name // this produces an error
);
setRelatedPosts(posts.results);
};
getPostData();
}, []);
How can I properly set the type from the union type?
Property name is not common to string and TagInterface.
Thus, you need to have two code paths for two cases:
tag is a string
tag is a TagInterface
TS will try to analyze your code and give you access to most precise type it inferred. The process is called Narrowing, and in some cases it can be fully transparent, and in some cases you need to check what is the actual type of the object you are dealing with.
In case of string | TagInterface union, the most straightorward approach is a typeof type guard.
interface TagInterface {
id: number;
name: string;
}
function getName(tag: TagInterface | string) {
if (typeof tag === 'string') {
return tag;
} else {
return tag.name;
}
}
console.log(getName('stringTag'));
console.log(getName({
id: 1,
name: 'tagInterfaceTag'
}));
Type assertion that you used in your onclick handler:
post.tags[0] as string is not a solution. With this assertion, you force TS to treat tag as a string - it will fail at runtime if it is not.
Playground link

Interface with default key value types that can be extended TypeScript

In many of my projects, I have a common pattern which repeats over and over again.
type RequestStatus =
| 'pending'
| 'requesting'
| 'successful'
| 'failed'
type AValue = string | undefined
type OtherValue = number
interface State {
requestStatus: RequestStatus;
aValue?: AValue;
otherValue?: OtherValue;
}
const [requestState, setRequestState] = useState<State>({
requestStatus: 'pending',
aValue: 'aValue',
otherValue: 11
});
const appendToState = (state: Partial<State>) =>
setRequestState(previousState => ({ ...previousState, ...state }));
In such a scenario, I am fetching values and changing the UI based on the requestStatus and the values.
This repeats over and over again in my projects and in an attempt to reduce the repetitions, I am considering wrapping all this in a hook. My main problem is dealing with types.
This was one of my attempts.
export const useExtendedRequestStatus = <T>(state: T) => {
const [requestState, setRequestState] = useState<{ requestStatus: RequestStatus } & T>({
requestStatus: 'pending',
...state,
});
const appendToState = (state: Partial<T>) =>
setRequestState(previousState => ({ ...previousState, ...state }));
return { requestState, setRequestState, appendToState };
};
And i would implement it like this
const {
requestStatus,
appendToState
} = useExtendedRequestStatus<{ requestStatus: RequestStatus; aValue: AValue }>({
aValue: aValue,
requestStatus: 'successful',
});
It works but not perfectly. There are instances if I am statically type checking the hook, I have to re-define the RequestStatus type which is already defined in the hook. I am wondering if there is a way, RequestStatus will still be in the hook when manually type checking without having to re-define it.
I am open to any ideas.
You indeed use a T generic in your useExtendedRequestStatus custom hook, which describes the possible members of the state you want to use it with.
But since you specify that the state argument, when you call your custom hook, is of type T directly, if you want it to include an initial requestStatus, then you are forced to also mention this requestStatus member in your explicit concrete type (of course you could rely on automatic type inference, but I guess you have your reasons to manually type check).
You can easily "embed" this predefined member in your custom hook, and have it available for your argument (same for appendToState), in a very similar manner you have already done with your inner useState<{ requestStatus: RequestStatus } & T>({...}): simply specify that your state argument is also of type T & { requestStatus: RequestStatus } (or maybe even better T & Partial<{ requestStatus: RequestStatus }> in case requestStatus can be initially omitted, as suggested by your initial default value in your custom hook). That way, T no longer needs to contain the requestStatus member. Same for appendToState.
interface IRequestStatus {
requestStatus: RequestStatus;
}
export const useExtendedRequestStatus = <T>(
state: T & Partial<IRequestStatus> // Initial state can contain requestStatus, even if not mentioned in concrete T
) => {
const [requestState, setRequestState] = useState<IRequestStatus & T>({
requestStatus: 'pending',
...state,
});
const appendToState = (state: Partial<T & IRequestStatus>) =>
setRequestState((previousState) => ({ ...previousState, ...state }));
return { requestState, setRequestState, appendToState };
};
Now in your React functional component, you can use it like this:
const { requestState, appendToState } = useExtendedRequestStatus<{
//requestStatus: RequestStatus; // Can now be omitted
aValue: AValue;
}>({
aValue: aValue,
requestStatus: 'successful', // Okay
});
requestState.requestStatus; // Okay
appendToState({
requestStatus: 'successful', // Okay
});
Demo: https://stackblitz.com/edit/react-ts-eyyzhg?file=Hello.tsx

Implement specfic behaviour based on type in Typescript

I am building a React site in which I want to display articles (I call them "Previews").
Articles can be either text or images/videos. I declared two interfaces (TextPreviewProps and ImgVidPreviewProps) that share some common properties (id, type) but also differ on others (TextPreviewProps has a text property while ImgVidPreviewProps has a file property for instance).
export interface TextPreviewProps {
id: number;
type: "text";
text: string;
onChangeText?: (newText: string) => void;
}
export interface ImgVidPreviewProps {
id: number;
type: "image" | "video";
file: File;
}
export type ContentPreviewProps = TextPreviewProps | ImgVidPreviewProps;
I have declared a type alias ContentPreviewProps that can be either a TextPreviewProps or a ImgVidPreviewProps.
My function that renders a "Preview" takes a ContentPreviewProps as input, and I want it to return a different React Component based on whether the input was Text or Image/Video. Since we cannot truly dynamically typecheck in Typescript, what is the best way to do this ?
At the moment, I check whether the type property is "text" or "image"/"video", and I then cast the ContentPreviewProps object to either a TextPreviewProps or a ImgVidPreviewProps based on the value of type.
const renderPreview = (
content: ContentPreviewProps
): JSX.Element => {
const { type, id } = content;
if (type === "text") {
const textContent = content as TextPreviewProps;
return (
<TextPreview
type={type}
text={textContent.text}
onChangeText={(newText: string) => console.log(newText)}
id={id}
/>
);
}
const imgVidContent = content as ImgVidPreviewProps;
return (
<ImgVidPreview
type={type}
file={imgVidContent.file}
id={id}
/>
);
};
However, there is redundancy in this approach, as I have to both check the type and then cast the object based on the result of that check.
What is the idiomatic way to implement such type-specific behaviour at runtime in Typescript ?
When you do that:
export type ContentPreviewProps = TextPreviewProps | ImgVidPreviewProps;
Then, as from the prop type in both TextPreviewProps and ImgVidPreviewProps will determine which of these will be pointed at in type ContentPreviewProps.
So, when you recieve prop type: string = "text" the ContentPreviewProps would idiomatically be:
{
id: number;
type: "text";
text: string;
onChangeText?: (newText: string) => void;
}
Thus, when used, you don't have to cast depending on type prop value! since you already did that when you declared that type:
export type ContentPreviewProps = TextPreviewProps | ImgVidPreviewProps;
You may modify you code as:
AS from Ewaren update below, destructring the type out of content was the cause of the problem.
const renderPreview = (
content: ContentPreviewProps
): JSX.Element => {
if (content.type === "text") { // TS won't complain about textContent.text since you are validating it here.
return (
<TextPreview
type={content.type}
text={content.text}
onChangeText={(newText: string) => console.log(newText)}
id={content.id}
/>
);
}
return (
<ImgVidPreview
type={content.type}
file={content.file}
id={content.id}
/>
);
};
I believe TS won't complain about any of this. since you are conditionally checking type.

Resources