TS allows strings to be string arrays? - 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.

Related

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

Argument of type 'Promise<Modules>' is not assignable to parameter of type '(params: any) => Promise<Modules>'

I have this helper function what I want to call after, with this signature :
export const fetchPaginated = async <T>(
callback: (params: any) => Promise<T>,
{
page = 1,
pageSize,
}: { page?: number; pageSize?: number; [x: string]: any },
previousData = [] as Array<T>
): Promise<T | undefined>
I trying to use it this way:
...
fetchPaginated<Modules>(getModules({}), {}),
...
When I call it is shows me always the same error:
Argument of type 'Promise<Modules>' is not assignable to parameter of type '(params: any) => Promise<Modules>'.
I already tried to changes the types, but nothing fixed the error
Argument of type 'Promise<Modules>' is not assignable to '(params: any) => Promise<Modules>'
This means that getModules({})
is not of type (params: any) => Promise<Modules>
is of type Promise<Modules>
More
Based on the error, it seems the fix will be:
...
fetchPaginated<Modules>(getModules, {}),
...

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

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});
}
}

Get error TS2322: Type 'any' is not assignable to type 'never'

When I do this, I get “ts-ignore - error TS2322: Type 'any' is not assignable to type 'never'
type Obj = {
key1: number;
key2: string;
key3: string | null;
};
type Keys = keyof Obj;
function mutateObj(obj: Obj, key: Keys, value: any) {
obj[key] = value;
}
I get the error above. When I remove null from key3, it works fine. if you do key3?: string; I get the same error. I know I am doing something wrong, and it relates to null / undefined type, but not sure how to resolve.
Update: Thanks for the implementation.
The actual use case is inside immer reducer in react app,
and I wonder if there is a way to type this without helper function.
(for now, I am using the function to mutate the draft in the first answer. Thanks a lot! :-) )
const myReducer = produce((draft:<Obj>, action:{type:”update_prop”; prop: Keys, value: any })=>{
switch (action.type) {
case “update_prop” :
// below gets type error
draft[action.prop] = action.value;
return;
}
})
You could implement it this way
type Obj = {
key1: number;
key2: string;
key3: string | null;
};
function mutateObj<T extends Obj, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}
const o: Obj = {
key1: 1,
key2: '2',
key3: null,
};
mutateObj(o, 'key1', 'zz');
Additionally it would retain type-safety of the values.

Typescript: Define type for object param of arrow function

I'm trying to define the type for object parameter in a arrow function, i'm new in typescript and i did find any exemple showing this case.
so:
const audioElem = Array.from(videoElem.parentNode.childNodes).find(
(child: {tagName: string}): boolean => child.tagName === 'AUDIO',
);
I'm just getting an error and that's normal but you got the idea.
Any idea ? :)
Error:
error TS2345: Argument of type '(child: { tagName: string; }) => boolean' is not assignable to parameter of type '(value: {}, index: number, obj: {}[]) => boolean'.
Types of parameters 'child' and 'value' are incompatible.
Property 'tagName' is missing in type '{}' but required in type '{ tagName: string; }'.
17 (child: { tagName: string }): boolean => child.tagName === 'AUDIO',
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/components/Subscriber/Subscriber.tsx:17:17
17 (child: { tagName: string }): boolean => child.tagName === 'AUDIO',
~~~~~~~
'tagName' is declared here.
You can use a type of interface to do that.
// Either…
type MyParam = {
tagName: string
}
// … or…
interface MyParam {
tagName: string
}
// …and then.
const audioElem = Array.from(videoElem.parentNode.childNodes).find(
(child: MyParam): boolean => child.tagName === 'AUDIO',
);
As for the difference between the two, you can read this article.

Resources