Constraint on return and error to make sure one is defined - reactjs

I have a simple case, hook function that returns {data, error} For simplicity lets assume that both are strings. While both error and data can be undefined when the second is returned, It's not possible that both are undefined together.
Do I have a way in typescript to set constraint that it will never happen that both are undefined? I use function in the React component and I would like this code
const myComponent: React.FC= function(){
const { data, error } = useMyCustomHook();
if ( error ) return<div> error </div>
if (data) return <div> data </div>
}
to be ok. Instead it throws
Type 'Element | undefined' is not assignable to type 'ReactElement<any, any> | null'.
Which wouldnt be here if there would be a way to tell typescript that error || data is always true.
One solution would be to make two interfaces one that require error with optional dataanother that require data leaving error optional and then type useMyCustomHook return to be union of those two, but it sounds like a bad solution that doesnt work in the scale. Imagin what will happen if I would like to have one property out of five, or even two out of five to be defined.

It would be useful to see what type useMyCustomHook returns.
If data can be falsy, such as undefined, the current code will miss one condition. In this case, you should handle the case where data is falsy.
You can modify the code like this:
const MyComponent: React.FC = () => {
const { data, error } = useMyCustomHook();
if (error) return <div>error</div>;
if (!data) return <div>empty</div>;
// you can safely access to the data
return <div>data</div>;
};
Alternatively, you can use a regular function as a component, so as not to be restricted to the React.FC type. This should still be a valid component:
const MyComponent = () => {
const { data, error } = useMyCustomHook();
if (error) return <div>error</div>;
if (data) return <div>data</div>;
}

Related

Type 'undefined' is not assignable to type 'Element | null'

I use a component :
const data: any[] = []
<Tiers data={data}/>
My component is built like this:
const Tiers = ({
data,
}: {
data?: any;
}) => {
console.log('data', data?.length!);
if(!data.length) return undefined;
};
export default Tiers;
I get the following message automatically :
'Tiers' cannot be used as a JSX component.
Its return type 'undefined' is not a valid JSX element.
Use
return null
instead of
return undefined
to make Tiers a valid JSX component.
You need two return statements in the component like below:
const Tiers = ({
data,
}: {
data?: any;
}) => {
console.log('data', data?.length!);
if(!data.length) return null;
return null
};
export default Tiers;
Your code has two problems,
It needs at least one condition that returns a JSX.Element(even a very simple one like Hello), if not, you probably needed a function instead.
A component cannot return undefined. If you want it to return nothing, you should return null instead of undefined.

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.

Typescript React: generic-based callback event handler function as prop to component

I'm having trouble creating a generic-based callback event handler function that I want to pass as prop down to my component.
My goal: Allow user to pass a custom callback function that:
always takes in the same argument
an event (not a react/dom event handler event, its coming from a library)
can return different return type
for my use cases, this component is used in different context's so in one place a user will return a certain value over another
What I have Attempted
I have my onNodeClick function defined using generics & when i use it in isolation, it works.
// ✅ Simple example of calling a generic function
const onNodeClick = <T,> (event:any) => {
return null as unknown as T;
}
const string_result = onNodeClick<string>(event_from_somewhere)
However, when I try to pass this method as a prop to my component, I am getting errors. I'm unsure of how to resolve it
Live Typescript Code Playground
import React from 'react';
type NodeComponentProps = {
onNodeClick: <T>(event: any) => T;
};
export const NodeComponent = ({onNodeClick}: NodeComponentProps) => {
return null;
}
const Homepage = () => {
const handleNodeClick = <T,>(event: any): T => {
return null as unknown as T;
};
return (
<NodeComponent
onNodeClick={(event): string => {
const string_result = handleNodeClick<string>(event); // ✅ correct type
return string_result; // ❌ onNodeClick is throwing type error; see error message a few lines below
}}
/>
)
}
/*
Type '<T>(event: any) => string' is not assignable to type '<T>(event: any) => T'.
Type 'string' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'string
*/
I think you want the type to be generic, not the function. If the function is generic, then it should be callable with angle brackets and work correctly. For example, the identity function works with any type, so it should pass your generic function definition:
const Homepage = () => {
const onNodeClick = <T>(x: T): T => {
return x;
};
return (
<NodeComponent
onNodeClick={onNodeClick}
/>
)
}
However, the function you used will always return a string. If you tried to call it with <number>, it would fail. Therefore, not a generic function.
By contrast, if you make the type generic, you should be able to specify you are using it with a string:
Here is a Playground link
type NodeComponentProps<T> = {
onNodeClick: (event: any) => T;
};
export const NodeComponent = <T,>({onNodeClick}: NodeComponentProps<T>) => {
return null;
}

Object is possibly 'undefined'. TS2532

I keep getting Object is possibly 'undefined'. TS2532 and I know that it's due to assignment of setViewer to undefined, which I have done for the purpose of login matching but I came across with another problem. Right now, I have 2 data coming in homeviewer and one of them is announcements and when I try to access the data, it gives me TS2532 error. Here are the code below. I am new to these and I don't know what to change for the setViewer (setViewer: () => undefined) in order it for to work.
homeViewer.ts
import { createContext } from 'react';
import { HomeViewer } from '../types/home_viewer';
export interface ViewerContextType {
viewer?: HomeViewer;
setViewer: (home: HomeViewer) => void;
}
export const ViewerContext = createContext<ViewerContextType>({
viewer: {},
setViewer: () => undefined,
});
That takes interface from
export interface HomeViewer {
user?: User;
announcements?: Announcement;
}
The part where I use it
const { viewer } = useContext(ViewerContext);
const data = viewer?.announcements;
console.log(data[0]);
If I understand correctly, your issue is revolving around accessing elements at specific indexes from data – where data possibly has a value of undefined (as per your HomeViewer interface);
export interface HomeViewer {
user?: User;
announcements?: Announcement;
}
To me, it appears that the Announcement type is indicative of an array of some elements (judging by the way you're accessing its data), or undefined (since it's optional).
Meaning, it will never certainly be an instance of Announcement's type declaration, so you'll have to chain an optional access operator for accessing array elements that possibly don't exist: ?.;
const { viewer } = useContext(ViewerContext);
const data = viewer?.announcements;
console.log(data?.[0]) // -> Ok!
console.log(data[0]) // -> Err! Object is possibly undefined.
It could be because if viewer is undefined then data will also be undefined, and you can't do data[0] because is the same as undefined[0]

Typescript: how to declare a type that includes all types extending a common type?

TLDR: Is there a way in Typescript to declare a type that encompasses all types that extend a given interface?
My specific problem
I am writing a custom React hook that encapsulates logic for deciding whether or not an element is moused over. It is modelled roughly after this hook. It exposes a ref that should be able to take any HTMLElement:
const ref = useRef<HTMLElement>(null);
The problem is, if I try to use this ref on any specific React element, I get an error telling me that this specific element is not quite HTMLElement. For example, if I use it with HTMLDivElement, I get this error: argument of type HTMLElement is not assignable to parameter of type HTMLDivElement.
Here's a simple repro case of the problem above in Typescript playground
Obviously, I wouldn't want to list types of all html elements in my hook. Given that HTMLDivElement extends the HTMLElement type, is there a way of declaring that the type that I am actually after is not strictly HTMLElement, but whatever extends HTMLElement?
React code example
source code of the hook
import { useRef, useState, useEffect } from 'react';
type UseHoverType = [React.RefObject<HTMLElement>, boolean];
export default function useHover(): UseHoverType {
const [isHovering, setIsHovering] = useState(false);
let isTouched = false;
const ref = useRef<HTMLElement>(null); // <-- What should the type be here?
const handleMouseEnter = () => {
if (!isTouched) {
setIsHovering(true);
}
isTouched = false;
};
const handleMouseLeave = () => {
setIsHovering(false);
};
const handleTouch = () => {
isTouched = true;
};
useEffect(() => {
const element = ref.current;
if (element) {
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
element.addEventListener('touchstart', handleTouch);
return () => {
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
element.removeEventListener('touchend', handleTouch);
};
}
}, [ref.current]);
return [ref, isHovering];
}
which produces type error if used like this:
import useHover from 'path-to-useHover';
const testFunction = () => {
const [hoverRef, isHovered] = useHover();
return (
<div
ref={hoverRef}
>
Stuff
</div>
);
}
Type error in example above will be:
Type 'RefObject<HTMLElement>' is not assignable to type 'string | RefObject<HTMLDivElement> | ((instance: HTMLDivElement | null) => void) | null | undefined'.
Type 'RefObject<HTMLElement>' is not assignable to type 'RefObject<HTMLDivElement>'.
Property 'align' is missing in type 'HTMLElement' but required in type 'HTMLDivElement'.
I think you are mistaken about the direction of the assignment that fails. If you have an interface A, then the type that matches all subclasses of A is just called A. This way, HTMLElement (i.e. is assignable from) any HTML element, e.g. HTMLDivElement.
This means that if you have a bunch of functions, one of them accepts HTMLDivElement, another accepts HTMLLinkElement etc, then there is no real type that you can pass to all of them. It would mean you expect to have an element that is both a div and a link and more.
Edited based on your edits of the question:
If the code you have works fine, and your only problem is that it doesn't compile, then just make your useHover generic, like this:
type UseHoverType<T extends HTMLElement> = [React.RefObject<T>, boolean];
function useHover<T extends HTMLElement>(): UseHoverType<T> {
const ref = useRef<T>(null); // <-- What should the type be here?
...
And then:
const testFunction = () => {
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
Something like this will make your code compile fine, without changing its runtime behaviour. I'm unable to tell if the runtime behaviour right now is as desired.
It works as expected, since HTMLDivElement extends HTMLElement. In your typescirpt playground you mixed it up. I updated it by switching x and y in this playground. You want the function to extend HTMLElement and pass y, which is and HTMLDivElement into it. And that works.

Resources