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

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

Related

Why is encumbranceByType object possible undefined?

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

How to type a type paramater in react Typescript?

I'm trying to create a generic hook that returns a function that clears the filter values.
I'm not passing any parameters, just a type parameter. FilterParams will be in the shape of example SomeFilterParams.
//...
export const useBuildClearValue = <FilterParams>() => {
const {
setFieldValue,
} = useFormikContext();
const buildClearValue = useCallback((inputName: keyof FilterParams) => {
return () => {
setFieldValue(inputName, null); //error is here
};
}, [
setFieldValue,
]);
return buildClearValue;
};
//... USAGE
type SomeFilterParams = {
name?: string;
status?: string;
}
const buildClearValue = useBuildClearValue<SomeFilterParams>();
TS is not happy with inputName in the setFieldValue.
Error: Argument of type 'string | number | symbol' is not assignable to parameter of type 'string'. Type 'number' is not assignable to type 'string'.ts(2345)
I guess that I need to type the passed type itself and enforce the key as a string and value as a string as well.
Any idea how this can be achieved?
You didn't show what the signature of setFieldValue is, but based on the error and your example usage, it looks like it takes a property name as a string and a value to set for that property. However, keyof T without any constraints on T is defined as any of the types that are valid for keys in JavaScript, which is strings, numbers, and symbols. Because you can't pass a number or symbol to setFieldValue, you get this error.
What you want is to restrict the input to only the string property names of your generic type. You can do that by filtering all number and symbol keys out using a conditional mapped type.
Here's a simplified example, removing the React and Formik-specific parts.
declare const setFieldValue: (property: string, value: unknown) => void;
export const useBuildClearValue = <T,>() => {
return (inputName: Exclude<keyof T, number | symbol>) => {
return () => {
setFieldValue(inputName, null);
};
};
};
type SomeFilterParams = {
name?: string;
status?: string;
}
const buildClearValue = useBuildClearValue<SomeFilterParams>();
buildClearValue("name");
buildClearValue("status");
TS Playground

How to use typescript generics constraints with useState

I am learning Typescript and have a custom React hook similar to this:
import { useEffect, useState } from 'react';
type TypeOne = {
one: string;
};
type TypeTwo = {
two: number;
};
type TypeThree = {
three: {
some: string;
};
}
type AnyPropertyWithString = {
[index: string]: string | AnyPropertyWithString;
};
export function getContent(condition: string): Promise<AnyPropertyWithString> {
return Promise.resolve({ one: 'content' });
}
export default function useContent<T extends AnyPropertyWithString>(
initial: T,
condition: string
): T {
const [content, setContent] = useState<T>(initial);
useEffect(() => {
async function fetchData() {
const data = await getContent(condition);
setContent(data);
}
fetchData();
}, [condition]);
return content;
}
My intention is to provide different types of data to this hook initially and save it in a state. Then fetch new data on some condition and replace the state. I want to restrict the type provided to the hook to AnyPropertyWithString. I want to use generic because the types I will provide could have different properties and multiple levels. The return type should be the same as generic.
I expect to use it like this:
const one: TypeOne = { one: 'content' };
const two: TypeTwo = { two: 2 };
const three: TypeThree = { three: { some: '3' } }
const firstResult = useContent(one, 'condition'); // ok
const secondResult = useContent(two, 'condition'); // expected TS error
const thirdResult = useContent(three, 'condition'); // ok
However, when I try to setContent(data), I have an error:
Argument of type 'AnyPropertyWithString' is not assignable to parameter of type 'SetStateAction<T>'.
Type 'AnyPropertyWithString' is not assignable to type 'T'.
'AnyPropertyWithString' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'AnyPropertyWithString'.
I don't want to cast it to T as I've read that this is a bad practice.
I suspect the problem is that getContent returns the data of the type that can't be effectively matched against, but I thought that generic constraint would narrow down T to AnyPropertyWithString (which is returned by getContent).
I tried useState<T | AnyPropertyWithString>, but then I have a problem with the return type. I also tried different combinations of extends, but none worked.
Could you, please, explain why I have this error and how I can work around it if that's possible?
I really appreciate any help you can provide.
How is getContent supposed to know what T is, and then return data in the format of T?
When you do this:
const hookReturnValue = useContent({ foo: string }, 'condition')
Then the T type in useContent is { foo: string }, which is a subtype of AnyPropertyWithString. getContent returns an entirely different subtype of T.
So if this code ran, then the initial value would set T to { foo: string }, then the effect would run and you would save { one: string } to state, which is not a compatible type.
Then your code would do:
const hookReturnValue = useContent({ foo: string }, 'condition')
hookReturnValue.foo.toUpperCase() // crash when effect completes
This is what is meant by this error:
'AnyPropertyWithString' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'AnyPropertyWithString'.
It's unclear how getContent would work, but it would need to know what kind of data format to return at runtime, and you aren't passing it anything that would let it figure that out.
So if you figure out how getContent will return the same type as the initial argument type of the hook, then the answer will start reveal itself.

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

React Hooks useState initial empty array value in typescript

I'm trying to make typescript happy by giving the correct type. But I can't figure out the exact syntax.
export type CategoriesType = {
id: string;
title: string;
url: string;
courses: CoursesType[];
};
const [singleCategory, setSingleCategory] = useState<CategoriesType>([] as any);
useEffect(() => {
const categories = Categories;
for (const category of categories) {
setSingleCategory(category);
}
}, [singleCategory]);
return (
singleCategory.courses !== [] ? ...
)
This code works. But typescript complains about usage of any. I would prefer to avoid using any.
Other tries:
I get error Property 'courses' does not exist on type 'CategoriesType | []'
const [singleCategory, setSingleCategory] = useState<CategoriesType | []>([]);
Object is possibly 'undefined'.
const [singleCategory, setSingleCategory] = useState<CategoriesType>();
Set state like below.
const [singleCategory, setSingleCategory] = useState<CategoriesType[]>([])
Also in your return singleCategory.courses doesn't make any sense because singleCategory is array type not the object type. You should match your types for state definition and usage.

Resources