Defining a generic based on react prop - javascript

I'm trying to define a generic type based on the type of a prop.
The implementation looks something like
interface ChildrenProps<T> {
values: T;
errors: T;
}
interface FormHandlerProps<T> {
initialValues: T;
children: React.FC<ChildrenProps<T>>;
}
export function FormHandler<T>({
children,
initialValues,
}: FormHandlerProps<typeof initialValues>) {
const [values] = useState<typeof initialValues>(initialValues)
return(
<>
children({values})
</>
)
}
However when I implement as below, 'values' is defined as 'any'
<FormHandler
initialValues={{name: 'test', address: 'test'}}
>
{({values}) => (
<p> {values.name} </p>
<p> {values.address} </p>
<p> {values.foobar} </p> // SHOULD BE INVALID
)}
</FormHandler>

You can't define the type of initialValues in terms of typeof initialValues; the compiler sees that as circular and doesn't know what to do. It bails out by giving it the any type:
export function FormHandler<T>({
children, initialValues
// -----> ~~~~~~~~~~~~~
// 'initialValues' implicitly has type 'any' because it does not have a type
// annotation and is referenced directly or indirectly in its own initializer.
}: FormHandlerProps<typeof initialValues>) { /* ... */ }
You should have seen a compiler warning to this effect as shown above, assuming you are using the --noImplicitAny or --strict compiler options. If you're not, you really should consider it, since it does a good job of catching errors.
Anyway, the fix here is to just give the argument an explicit type that is not self-referential:
export function FormHandler<T>({
children, initialValues
}: FormHandlerProps<T>) {
const [values] = useState<typeof initialValues>(initialValues)
return (
<>
children({values})
</>
)
}
And then the rest of your code should hopefully work as expected.
Playground link to code

Related

Is there a way to pass type to generic component before using it?

export const ComponentsOverrideContextProvider= <M extends Record<any, any>>({children, components}: {children: React.ReactNode, components: M}) => {
return <ComponentOverrideContext.Provider value={{ ...components }}>{children}</ComponentOverrideContext.Provider>;}
i have a component above which is generic and it should not be tight to a specific implementation, or order to avoid it i am doing something like
// concrete implementation
export const ConcreteProvider = ComponentsOverrideContextProvider;
and I need to somehow pass type to ComponentsOverrideContextProvider before end user uses it. Is there any way ?
user should not be doing like this
<ConcreteProvider<Type>></ConcreateProvider>
it should be typed before he calls this component
Simply specify the concrete type when doing your concrete implementation:
export const ConcreteProvider =
ComponentsOverrideContextProvider<
// Pass the concrete type
Record<string, number>
>;
() => (
<ConcreteProvider components={{
foo: 0,
bar: "hello" // Error: Type 'string' is not assignable to type 'number'.
// ~~~
}}>
Some children
</ConcreteProvider>
)
Playground Link

Typescript - Conditional Type / Optional

I have a component, It takes arguments such as :
interface Props {
label: string;
children?: React.ReactNode;
withoutActions?: boolean;
fieldKey?: KeyProperties;
corporate: Corporate;
}
The withoutActions and fieldKey are used by a form.
If the withoutActions prop is true, then there should not be a provided fieldKey.
However, if my withoutActions is undefined, then i should enforce my fieldLabel to be of type **KeyProperties** which is a specific list of available values for it.
If withoutActions is undefined, the other SHOULD NOT be defined at all
If withoutActions is true, the other SHOULD be defined respecting the specific keyProperties type.
How can i implement that ?
Similar to the other answer, but you can refactor it a bit to be cleaner. You did not specify what the expected behavior is if withoutActions is false instead of true or undefined. Here, I assume the behavior for false and undefined are the same. If false is not a valid type, you could just swap withoutActions?: false for withoutActions: undefined like the other answer.
type Props = {
label: string;
children?: React.ReactNode;
corporate: Corporate;
} & (
{
withoutActions: true;
} |
{
withoutActions?: false;
fieldKey: KeyProperties;
}
)
However, there is an important pitfall here that you should be aware of. Because of TypeScript's structural typing, you only get excess property checking when you are directly assigning an object literal. You do not get excess property checking when you assign an an object as an inferred type. TypeScript and React treat direct props declarations as if they are object literals, and will do excess property checking like you seem to desire. However in some cases, if you assign objects to variables and let their type be inferred, TypeScript may not warn that there is an excess property present.
Check out this demo based on your original example. Example #1 and #2 will error because of excess property checking, but example #3 will not.
const ExampleOne = () => {
// Type error - excess property checking
return <Component label={''} corporate={''} withoutActions fieldKey={''} />;
}
const ExampleTwo = () => {
const props: Props = {
label: '',
corporate: '',
withoutActions: true,
// Type error - excess property checking
fieldKey: '',
}
return <Component {...props} />;
}
const ExampleThree = () => {
const props = {
label: '',
corporate: '',
withoutActions: true,
fieldKey: '',
}
// No type error - no excess property checking
return <Component {...props} />;
}
I did not fully understand your requirement, but I would use a type alias instead of an interface. E.g. something like this:
type Props = {
label: string,
children?: React.ReactNode,
withoutActions: true,
corporate: Corporate
} | {
label: string,
children?: React.ReactNode,
withoutActions: undefined,
fieldKey: KeyProperties,
corporate: Corporate
}

How to use either this type or another based on props only? React Typescript Discriminating Unions

I have a componenet:
type Base = {
color: string
}
type Button = {
to: string
} & Base
type Link = {
link: string
linkNewTab: boolean
} & Base
type ComponentProps = Button | Link
export const Button: React.FC<ComponentProps> = (props) => {
return (<>
{props.link ? <Link newTab={props.linkNewTab}>...</Link> : props.to && <Button>...</Button>}
</>)
}
There is a base type with props which both types have in common.
The Component should either has Button or Link type based on the given props. But if it has Link, these props are preferred.
Typescript Error:
Property 'link' does not exist on type 'PropsWithChildren<ComponentProps>'.
Property 'link' does not exist on type '{ to: string; } & Base & { children?: ReactNode; }'.ts(2339)
What I don't want:
I can solve the problem by adding a type to the Base type and decide from their which props are allowed. I would like to automatically decide that based on props.
Information: 4.5.4 TypeScript Version
The problem is that you are attempting to access a value at property which might not exist. Instead, you can check to see if that property exists in the object (by using the in operator) before trying to access its value. This will also discriminate the union:
TS Playground
// instead of this:
if (props.link) {}
// ^^^^
// Property 'link' does not exist on type 'Button | Link'.
// Property 'link' does not exist on type 'Button'.(2339)
// prop in obj
if ('link' in props) {
props.color // ok
props.link // ok
props.linkNewTab // ok
}
else {
props.color // ok
props.to // ok
}
You can check whether the link property exists by using the in operator. This will narrow down the type without accessing the property, and so typescript will allow it:
<>
{"link" in props ? (
<Link newTab={props.linkNewTab}>...</Link>
) : (
"to" in props && <Button>...</Button>
)}
</>

Checking one conditional React prop does not satisfy TS

I'm having trouble understanding Typescript. I want to define a <Component/> with one required prop requiredProp, and a condition prop extend which if true, allows to use extendedProp.
e.g.
<Component requiredProp={''} /> // OK
<Component requiredProp={''} extend={true} requiredProp={Function} /> // OK
<Component requiredProp={''} requiredProp={Function} /> // ERROR
<Component requiredProp={''} extend={true} /> // ERROR
My code:
// types
interface RequiredProps {
requiredProp: 'string';
}
type ExtendedProps =
| {
extend?: false;
extendedProp?: never;
}
| {
extend: true;
extendedProp: Function;
};
type ComponentProps = RequiredProps & ExtendedProps;
// component
const Component: React.FC<ComponentProps> = ({requiredProp, extend, extendedProp}) => {
if (extend) {
extendedProp(); // ERROR: Cannot invoke an object which is possibly 'undefined'.
}
}
If I am checking extend to be true, shouldn't TS automatically know from ExtendedProps that extendedProp is also defined?
Compiler is only satisfied if I am explicitly checking extendedProp to be defined:
if (extendedProp) {
extendedProp(); // no error
}
If you do this using the entire props object, it will work. checking for props.extend will narrow down the type on props, and thus props.extendedProp will be allowed:
const Component: React.FC<ComponentProps> = (props) => {
if (props.extend) {
props.extendedProp();
}
}
But if you've already split them up into separate variables, then this can't happen. props isn't involved in your if statement at all, so the type on props can't be narrowed. There are just two unrelated local variables, and checking the type on one doesn't do anything to the type on the other.
You and i can recognize that the two variables are related to eachother, but typescript doesn't have the ability to back track to find the origin of the type, and deduce what that means for other types. It may seem simple in this case, but keep in mind that there are a huge variety of lines of code that could be written that would break the linkage, such as:
const Component: React.FC<ComponentProps> = ({requiredProp, extend, extendedProp}) => {
extend = Math.random() > 0.5
if (extend) {
extendedProp();
}
}
Figuring out the implications of those kinds of things is impractical (or maybe impossible);
Playground link

Cannot invoke an expression whose type lacks a call signature. Type 'number | Dispatch<SetStateAction<number>>' has no compatible call signatures

I'm setting up state to be passed through context in React (with hooks). When using the dispatched state updater function, I get this error:
Cannot invoke an expression whose type lacks a call signature. Type 'number | Dispatch<SetStateAction<number>>' has no compatible call signatures
I'm pretty new to TypeScript, and I think that I might be just missing a type somewhere, but I'm stuck 😅
Initially I was trying to pass the defaultValue (which in my case is a number) and then tried passing the state and updater into the Provider's value. BUT... the type is inferred from that context's defaultValue - which is required (for some reason). So that doesn't work...
I'm passing the value to both React.createContext() and PatsContext.Provider. Again, because a defaultValue is required in React.createContext():
import * as React from 'react';
const [pats, givePats] = React.useState(0);
const value = React.useMemo(() => [pats, givePats], [pats]);
const PatsContext = React.createContext(value);
function PatsProvider() {
return <PatsContext.Provider value={value} />;
}
function GoodBoi() {
const [, givePats] = React.useContext(PatsContext);
return (
<button onClick={() => givePats(prevPats => prevPats + 1)} type='button'>
Good Boi!
</button>
);
}
function PatsDisplay() {
const [pats] = React.useContext(PatsContext);
return <div>The good boi has received {pats} pats.</div>;
}
function GoodBoiExample() {
return (
<PatsProvider>
<PatsDisplay />
<GoodBoi />
</PatsProvider>
);
}
export default GoodBoiExample;
Trying to use givePats() yields the TypeScript error as described above.
Also, using <PatsProvider /> in the <GoodBoiExample /> component yields this error as well:
Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes'.
Not sure what that means. But I like to think that once I get passed the first error this will fall in line? ¯_(ツ)_/¯ Thanks!
Needed explicitly defined type:
type UpdateState = React.Dispatch<React.SetStateAction<number>>;
const [pats, givePats] = React.useState<number>(0);
const value = React.useMemo<[number, UpdateState]>(() => [pats, givePats], [pats]);
const PatsContext = React.createContext(value);
function PatsProvider({ children }) {
return <PatsContext.Provider value={value}>{children}</PatsContext.Provider>;
}
Passing children inside of the context provider infers the type any to children - as I think it should be? This solves the second error.
Alternatively I could've passed the initialValue as an object with my state and updater. Type inference would've been more explicitly number for the state and React.Dispatch<React.SetStateAction<number>> for the updater instead of being infered as (number | Dispatch<SetStateAction<number>>). However, the second error's solution remains the same.

Categories

Resources