Typescript String enum type 'string' is not assignable to type - reactjs

Link to Playground
Pretty simple, how do I avoid this compile error?
type ExampleClass = {
relevantValue: EXAMPLE_STRING_ENUM;
}
enum EXAMPLE_STRING_ENUM {
HELLO = 'hello',
}
const exampleArray: ExampleClass[] = [];
const mockData = {
relevantValue: 'hello'
}
exampleArray.push(mockData);
results in
Argument of type '{ relevantValue: string; }' is not assignable to
parameter of type 'ExampleClass'. Types of property 'relevantValue'
are incompatible.
Type 'string' is not assignable to type 'EXAMPLE_STRING_ENUM'.(2345)
In my head this should work. During writing code, I can have a switch/case statement depending on what a given field is, but the mockData comes from live, so of course it is not having an enum value, but directly the string value instead.
I thought since it is a string enum, typescript should know that the values can only be of type string?
I could just fix it by making a union type like this:
type ExampleClass = {
relevantValue: EXAMPLE_STRING_ENUM | string;
}
but this does not really solve the underlying problem, which I am still trying to fix.
Also, this 'fix' would lead to follow up errors like such:
exampleArray.forEach((e) => {
setFoo(e.relevantValue)
})
Argument of type 'string' is not assignable to parameter of type
'SetStateAction<EXAMPLE_STRING_ENUM | undefined>'.(2345)
EDIT: Solved it, by typing all values where the compiler cannot know whether the string provided really is matching one of the ENUM strings, so throwing the error made sense.
Updated code:
const [foo, setFoo] = React.useState<EXAMPLE_STRING_ENUM>();
type ExampleClass = {
relevantValue: string;
}
enum EXAMPLE_STRING_ENUM {
HELLO = 'hello',
}
const exampleArray: ExampleClass[] = [];
const mockData: ExampleClass = {
relevantValue: 'hello'
}
exampleArray.forEach((e) => {
setFoo(e.relevantValue as EXAMPLE_STRING_ENUM)
})

You need to add a guard to the mockData constant to let Typescript know that relevantValue is constrained to values in the enum:
const mockData: ExampleClass = {
relevantValue: EXAMPLE_STRING_ENUM.HELLO
}
If you don't do this then the following is allowed, which is why typescript errors in your example:
mockData.relevantValue = 'not an enum value';
playground

Related

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.

React.ComponentProps works differently than directly referencing exported type of props

I have this a component declared where I have defined the types for the props in a type alias.
type Props<RowType> = {
tableId: string;
columns: GridColumns;
rows: RowType[];
searchKey?: StringKeyOf<RowType>; // type StringKeyOf<T> = Extract<keyof T, string>;
dropdownFilters?: BasedOnData<SingleFilterConfig, RowType>[];
buttonGroupFilters?: BasedOnData<MultiFilterConfig, RowType>[];
tableFilterKey?: string;
};
const DataPresentation = <RowType extends AnyObject>({
rows,
columns,
tableId,
searchKey,
dropdownFilters = [],
buttonGroupFilters = [],
}: Props<RowType>) => { ... }
I'm using Storybook with CSF3, so I haven't figured out how to correctly type the stories themselves, but at least I should be able to type their arg parameter.
type Props = ComponentProps<typeof DataPresentation>;
const Default = {
args: {
...DataTableStories.Default.args,
tableId: 'playerProps',
} as Props,
};
const SearchKey = {
...Default,
args: {
...Default.args,
searchKey: 'playerName',
} as Props,
}
The above almost works. The Default story works fine, but something is strange about the error for the SearchKey story:
TS2352: Conversion of type '{ searchKey: string; tableId: string; columns: GridColumns<any>; rows: unknown[]; dropdownFilters?: BasedOnData<SingleFilterConfig, unknown>[] | undefined; buttonGroupFilters?: BasedOnData<...>[] | undefined; tableFilterKey?: string | undefined; }' to type 'Props<unknown>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.   
Types of property 'searchKey' are incompatible.     
Type 'string' is not comparable to type 'undefined'.
I'm not sure what to make of what it's saying about Props<unknown, although I will say that casting the args to as unknown as Props does work but that seems wrong. So I'm left with the issue of Type 'string' is not comparable to type 'undefined'.
When I put my cursor after the tableId in the Default story, it offers me suggestions. Notice that while tableFilterKey is shown as sting | undefined, searchKey is indeed shown simply as undefined.
There may be a question about the difference between string and StringKeyOf<RowType> (which should be a type containing all the string keys of RowType; even though RowType is said elsewhere to extend Record<string, any> I needed the Extract utility to allow keyof RowType to be seen as extending string).
However, if I export the type:
export type DataPresentationProps<T> = Props<T>;
And use this to cast the stories' args, Typescript is happy.
type Props = DataPresentationProps<typeof DataTableStories.Default.args.rows[number]>;
const Default = {
args: {
...DataTableStories.Default.args,
tableId: 'playerProps',
} as Props,
};
const SearchKey = {
...Default,
args: {
...Default.args,
searchKey: 'playerName',
} as Props,
Why is using the exported (literal) type successful and using ComponentProps isn't? Obviously there's way more to ComponentProps than just the type I've exported (I think) but I don't know what's going on in there that would change this.

Typescript Error: Expression of type 'string' can't be used to index type

I am new to Typescript and having an issue with types: I get the following error:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type...No index signature with a parameter of type 'string' was found on type
I have data being returned from an endpoint that looks as such:
export type CategoryPosts = {
count: number;
posts: Posts[]
}
export type PopularPosts = {
posts: {
food: CategoryPosts;
vacations: CategoryPosts;
travel: CategoryPosts;
music: CategoryPosts;
}
}
In a component, I have a function that is trying to access this data where a user can click on an article and I pass the postType which is a string to another function which will filter. I think because one is a string, but in my types they are keys it is throwing an error.
let selectedCategory = posts[postsType].posts <- TS error
selectedCategory.filter(posts => ....)
It all works as expected, but I can't resolve this typescript error and I cannot declare it as type any.
You need to tell typescript that the postType string is one of the type keys of your PopularPosts.posts type, so it won't try to use for example a string called "drama" that can't be indexed to the posts object because there's no key with that name and that is why typescript is complaining. You can do some extra typing in order to make this work.
type Posts = {
title: string
description: string
}
type CategoryPosts = {
count: number;
posts: Posts[]
}
type PopularPosts = {
posts: {
food: CategoryPosts;
vacations: CategoryPosts;
travel: CategoryPosts;
music: CategoryPosts;
}
}
// This will give your postType string the correct typing
// This return all the keys that are on your posts
type Category = keyof PopularPosts["posts"]
// Example of creating a const for the postsType
const postsType: Category = 'food'
let selectedCategory = posts[postsType].posts
If you post type is coming from props or for some reason you can't do the typing above you could use some casting to achieve what you want.
let selectedCategory = posts[postsType as Category].posts

Why can't I send prop as an object property in React component (typescript)?

I have a constant:
export const LIST_OF_TYPES = {
TYPE1: 'type1',
TYPE2: 'type2',
};
In the component, I define the type as follows:
export type IExampleComponentType = 'type1' | 'type2';
interface IExampleComponentProps {
type: IExampleComponentType;
}
const ExampleComponent: React.FC<IExampleComponentProps> = ({
type,
}) => {
...
When I try to send a prop to a component like this:
<ExampleComponent type={LIST_OF_TYPES.TYPE1} />
i see an error for prop type like this:
TS2322: Type 'string' is not assignable to type 'IExampleComponentType'.
The expected type comes from property 'type' which is declared here on type 'IntrinsicAttributes & IExampleComponentProps & { children?: ReactNode; }'
What's wrong? How can I fix this?
When you declare LIST_OF_TYPES like this, it's type is automatically derived as { TYPE1: string; TYPE2: string }, since Typescript supposes you can write some other values into LIST_OF_TYPES's properties in the future. You can prevent this using as const cast:
const LIST_OF_TYPES = {
...
} as const;
// also you can now declare IExampleComponentType like this:
type IExampleComponentType = (typeof LIST_OF_TYPES)[keyof typeof LIST_OF_TYPES];
Also you may use a enum here (and it seems like you need one):
enum LIST_OF_TYPES {
TYPE1 = 'type1',
TYPE2 = 'type2',
}
// also now IExampleComponentType can be replaced with just LIST_OF_TYPES
Check it out at the playground
The type of LIST_OF_TYPES is:
{
TYPE1: string;
TYPE2: string;
}
if you declare LIST_OF_TYPES with the as const modifier:
export const LIST_OF_TYPES = {
TYPE1: 'type1',
TYPE2: 'type2',
} as const;
its type will, instead be:
{
readonly TYPE1: "type1";
readonly TYPE2: "type2";
}
Which is, I believe, what you're looking for.
Further reading: https://stackoverflow.com/a/66993654/14357
A literal is a more concrete sub-type of a collective type. What this means is that "Hello World" is a string, but a string is not "Hello World" inside the type system. - Ts Doc
Typescript will infer string literal types only when you assign it to a const.
Understand it by example
const LIST_OF_TYPES = {
TYPE1: 'type1',
TYPE2: 'type2'
};
//string literal
type IExampleType = 'type1' | 'type2';
function myFunction(value: IExampleType) {
console.log(value);
}
//This will not work
myFunction(LIST_OF_TYPES.TYPE1);
Type Error: Argument of type 'string' is not assignable to parameter of
type IExampleType'.
//This will work
const myValue = 'type1';
myFunction(myValue);

Resources