How Can I Pass In The Name of the Key in setState()? - reactjs

I have two functions that duplicate code except for the name of the key in the call to setState():
fooFunc(value: Moment) {
if (condition){
this.setState({foo: value});
} else {
doSomethingElse();
this.setState({foo: undefined});
}
}
barFunc(value: Moment) {
if (condition){
this.setState({bar: value});
} else {
doSomethingElse();
this.setState({bar: undefined});
}
}
How can I refactor this to a single method that accepts the name of the key as a parameter and sets the state accordingly?
// something like this, but this doesn't work:
parameterizedFunc(name: string, value: Moment){
if (condition){
this.setState({[name]:value});
} else {
this.setState({[name]:undefined});
}
}
When using the above technique, I am getting the following (redacted) error message:
"Argument of type '{ [x: string]: moment.Moment; }' is not assignable to parameter of type 'MyState | Pick<MyState, keyof MyState> | ((prevState: Readonly<...>, props: Readonly<...>) => MyState | ... 1 more ... | null) | null'.\n Type '{ [x: string]: Moment; }' is missing the following properties from type 'Pick<MyState, keyof MyState>': aList, bList, cList, dList, and 8 more."

I found that I need to use the "previous state" overload of setState, and use it to populate the "missing" properties:
parameterizedFunc(name: string, value: Moment){
if (condition){
this.setState(prevState => {...prevState, [name]:value});
} else {
this.setState(prevState => {...prevState, [name]:undefined});
}
}

Related

TS allows strings to be string arrays?

I've narrowed down an issue to the code below which is supposed to return a mapping from strings to string arrays. However, in my reducer I create a mapping from strings to strings, and TypeScript compiles the snippet without error:
const parseFeatures = (featSpecs: string[]): Record<string, string[]> =>
featSpecs.map(f => f.split("=", 2))
.reduce((acc, [k, v]) => {
// v is a string, so why does this compile?
return {...acc, [k]: v};
}, {});
console.log(parseFeatures(["foo=bar", "baz=qux,stuff,thing"]))
// correctly doesn't compile: Type 'string' is not assignable to type 'string[]'.
// const foo: Record<string, string[]> = {"foo": "bar"}
Running the snippet in TS playground gives this output:
{
"foo": "bar",
"baz": "qux,stuff,thing"
}
This is clearly not a mapping from strings to string arrays! How do I make TypeScript correctly type-check my reducer? I want this snippet to emit errors.
The issue here is that you are providing the empty literal to the reduce function. The return type is inferred from the type of that initial value and thus .reduce() returns {} which is assignable.
You can fix it by defining a type for the init value:
function parseFeatures(featSpecs: string[]): Record<string, string[]> {
const init: Record<string, string[]> = {}
return featSpecs
.map(f => f.split("=", 2))
.reduce((acc, [k, v]) => {
// this return vallue does not fit the init value type
return { ...acc, [k]: v };
}, init);
}
will give you the desired error:
Overload 2 of 3, '(callbackfn: (previousValue: Record<string,
string[]>, currentValue: string[], currentIndex: number, array:
string[][]) => Record<string, string[]>, initialValue: Record<string,
string[]>): Record<...>', gave the following error.
Argument of type '(acc: Record<string, string[]>, elem: string[]) => { [x: string]: string | string[]; }' is not assignable to parameter of type '(previousValue: Record<string, string[]>, currentValue:
string[], currentIndex: number, array: string[][]) => Record<string,
string[]>'.
Type '{ [x: string]: string | string[]; }' is not assignable to type 'Record<string, string[]>'.
'string' index signatures are incompatible.
Type 'string | string[]' is not assignable to type 'string[]'.
TS types may just describe a subset, so if you add a property to {}, TS will still consider it safe because you will not have access the the added properties afterwards. And then {} is just an empty record, so no problem there.

How can I make argument assignable to proper parameter?

I am new to typescript and I have following error bound with setFilteredColumns(obj)
Argument of type '(false | Column)[]' is not assignable to parameter of type 'SetStateAction<Column[] | undefined>'.
Type '(false | Column)[]' is not assignable to type 'Column[]'.
Type 'false | Column' is not assignable to type 'Column'.
Type 'boolean' is not assignable to type 'Column'.ts(2345)
interface ColumnsHeader {
title: string;
disp: boolean;
}
export interface Column {
col: ColumnsHeader;
}
interface IProps{
tocompare:Column[];
columns:Column[]
}
const useBuildColumns = ({tocompare, columns}:IProps) => {
const [filteredColumns, setFilteredColumns] = useState<Column[]>();
useEffect(() => {
let obj = tocompare &&
tocompare.map((k:Column, ii:number) => {
return columns[ii] && columns[ii].col.disp === true && k;
})
setFilteredColumns(obj);
}, [columns]);
return [filteredColumns];
};
How can I resolve this error?
The problem is that you use logical operators everywhere, even where it leads to a distortion of the result. In your example map function can return false.
I didn't quite understand what was going in useEffect, maybe you tried to make something like this:
useEffect(() => {
if (tocompare) {
let obj = tocompare.filter(
(k: Column, ii: number) => columns[ii] && columns[ii].col.disp
);
setFilteredColumns(obj);
}
}, [columns]);
Remember that logical operators returns a boolean value

TypeScript | Can't loop over a custom type Object

I'm trying to return a [key, value] pair from a custom response data object, but when I try to loop over each key[value], it's gives me this error:
No index signature with a parameter of type 'string' was found on type 'IResponse'
or
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'IResponse'
here's my Detail.tsx:
interface IResponse {
birth_year: string;
created: string;
edited: string;
eye_color: string;
films: string[];
gender: string;
hair_color: string;
heigth: string;
homeworld: string;
mass: string;
name: string;
skin_color: string;
species: string[];
startships: string[];
url: string;
vehicles: string[];
}
const Detail = () => {
const [person, setPerson] = useState<IResponse>({} as IResponse);
const { id } = useParams();
useEffect(() => {
api.get(`people/${id}`).then((res) => {
if (res.data) setPerson(res.data as IResponse);
});
}, [id]);
function printValues() {
return Object.keys(person).map((key) => (
<li>
{key}:{person[key]}
</li>
));
}
return <ul>{printValues()}</ul>;
};
My question is: Why this code:
function objectEntries() {
const a = "name";
const entries = person[a];
console.log(entries);
}
objectEntries();
Works fine and in printValues function doesn't?
I see 2 different issues here. First you have to call print values return <ul>{printValues()}</ul>. Second you are not handling when person.data is empty on the first render before the data is fetched.
Update after comment: Alright since you have these the actual way to fix the type error is like this
let key: keyof IResponse['data'];
for (key in person.data) {
The issue is you do not have any string type keys. So you need to say you are using the keys of that object
If you dont like that syntax you can always use the following instead
for (let [key, value] in Object.entries(person.data))
I only need to say to TypeScript that the object can access a [key: string]: value: any on the IResponse interface!
like:
interface IResponse {
[key: string]: your-value-type
[...]
}

TSLint complains about property does not exist for an element of mixed typed array

I have the following piece of code:
interface TileA {
imageUrl: string;
}
interface TileB {
videoUrl: string;
}
interface TileC {
thumbnailUrl: string;
}
interface Curation {
items: (TileA | TileB | TileC)[];
}
const curations: Curation[] = SOME_DATA;
curations.map((curation) => {
curation.items.map((item) => {
if (typeof item.videoUrl != 'undefined') { // getting a TS2339 complaining videoUrl is not a property
// do something
}
});
});
and as shown, when I try to assign the property videoUrl to an item, TS complains about not a valid property?
I am guessing it's because it doesn't know which actual type item is? I tried cast it to a specific Tile but the casting also result in TSLint error.
I am not sure what's the best way to handle a mixed typed array?
Thanks!
The function in .map() should return a value.
Here's how you map a list of Bs to As:
const aItems = items.map((item: B): A => {
return {
...item,
age: 40
}
});
What happens here is that we clone the given item using spread syntax and assign the new age property to it.
Moreover, if it's not necessary to have both types A and B, you could also make age an optional property and use a single type for all items:
interface A {
name: string;
age?: number;
}
Edit 20/01/03:
interface Tile {
type: "image" | "video" | "thumbnail";
url: string;
}
...
curations.map((curation) => {
curation.items.map((item) => {
switch (item.type) {
case "image":
// do something
// handle case for each type
...
}
});

How to use typescript with dynamic object key

I have a State object that records time in days, hours, and minutes. I defined my state like this:
type StateKeys = "days" | "hours" | "minutes";
type State = {
[K in StateKeys]: number
};
Later, I want to set state based on a value changed in a control. There are three controls, one for day, another for hours, and a third for minutes. I have one handler function that is hooked up to each of these controls. Here's an excerpt of that function:
_onTimeComponentChange(e: any) {
const name : StateKeys = e.currentTarget.name;
const updatedValue = parseInt(e.currentTarget.value);
this.setState(
//#ts-ignore
{[name]: updatedValue},
() => {
this.updateValue();
}
)
}
My goal is to remove the //#tsignore comment. If I do that now, I get this error message:
Argument of type '{ [x: string]: number; }' is not assignable to
parameter of type 'State | ((prevState: Readonly, props:
Readonly) => State | Pick) | Pick'. Type '{ [x: string]: number; }' is missing the
following properties from type 'Pick': days, hours,
minutes
How do I remove the //#tsignorecomment and satisfy typescript's requirements?
It's definitely hard to get around this type of thing without casting. You can try following the pattern described here:
updateState(key: StateKeys, value: string) {
this.setState((prevState) => ({
...prevState,
[key]: value,
}));
}
which would look like:
_onTimeComponentChange(e: any) {
const name: StateKeys = e.currentTarget.name
const updatedValue = parseInt(e.currentTarget.value)
this.setState(
prevState => ({
...prevState,
[name]: updatedValue,
}),
() => {
},
)
}

Resources