'Same property type on different levels
I have a hook that accepts a type:
type TJsonData = {
message: string;
activity: string;
};
But this type can be on three different levels, so it can accept either:
{ message: "", activity: "" }{ someData: { message: "", activity: "" } }{ errors: { someError: { message: "", activity: "" } } }
I managed to make this work without any errors with:
type TLoaderData =
| TJsonData
| Record<string, TJsonData>
| {
errors?: Record<string, TJsonData>;
};
The problem is, inside the hook it can't properly determine the correct type, at least with my current implementation:
function useActionNotify(...actions: Array<TLoaderData | undefined>) {
//...
useEffect(() => {
actions.forEach((action) => {
if (!action) return;
if (
action.activity // Error here
) {
return notify(action);
}
Object.values(action).forEach((data) => {
if (data.activity) {
return notify(data);
}
// Data errors will be another object to be looped over
Object.values(data).forEach((error) => {
notify(error);
});
});
});
}, [actions, notify]);
}
With the above it gets a type error (or something similar):
Property 'activity' does not exist on type 'TLoaderData'.
Property 'activity' does not exist on type '{ errors?: Record<string, TJsonData> | undefined; }'.ts(2339)
How do I make it get the correct type? Is there any other way other than to force typecast, or is my TLoaderData wrong to begin with?
Codesandbox for the other errors and full implementation: https://codesandbox.io/s/wizardly-faraday-jxjirf?file=/src/App.tsx
Solution 1:[1]
You need user-defined type predicate to let TypeScript know which part of your TLoaderData type union is handled in each code branch:
/**
* Type predicate
* https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
*/
function isJsonData(obj: TLoaderData): obj is TJsonData {
// Can be implemented using a combination of `in` operator and `typeof` type guards
return 'activity' in obj && typeof obj.activity === 'string';
}
Then you use it in your if condition, and TypeScript automatically knows that within the corresponding block, your object is narrowed down to TJsonData:
if (isJsonData(action)) {
return notify(action); // Now is okay!
}
Just be careful with Object.values, which sometimes does not correctly infer the value type. In such case, you can resort to type assertion, or fallback to an old school for in loop (but you may still need to type assert the key!):
for (const key in action) {
const data = action[key as keyof typeof action]
if (isJsonData(data)) {
return notify(data); // Okay!
}
// Data errors will be another object to be looped over
Object.values(data).forEach((error) => {
notify(error); // Still okay!
});
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | ghybs |
