How can you declare a dictionary with keys of any type? - javascript

I have a dictionary where keys can be any possible value (including dates, objects, etc). I tried declaring it with { [x: any ]: number }, but I get the error "An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type." What would be the best way to approach this in Typescript?
// Aggregates the counts for various values of a particular key. Read-only. Does not mutate the dataset.
count = (field: keyof T) => {
var counter: { [x: any]: number } = {}
this.data.forEach( x => {
let value = x[field]
if( counter[value] ){
counter[value]++
}else{
counter[value] = 1
}
})
return counter
}

Related

How to iterate through object without type complain when using reduce method?

I'm trying to convert the following data [{ year: "yyyy" }, { month: "mm" }, { day: "dd" }] into this { year: "yyyy", month: "mm", day: "dd" }. So in the below code base, I used reduce method, but I'm getting type script error message on curr, saying "Type '{ [x: string]: any; }' is not an array type or a string type." Also on acc[key], I get another error message "Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}'" In other IDE, I don't get type error, but get this error on console "TypeError: Invalid attempt to iterate non-iterable instance.
In order to be iterable, non-array objects must have a Symbol.iterator method."
I know there are other ways to manipulate the data such as for loop, but I would like to know how I can get this working using reduce method. https://codesandbox.io/s/sharp-glitter-43ql15?file=/src/App.tsx:0-550
import "./styles.css";
import { useEffect } from "react";
export default function App() {
interface IData {
day: string;
month: string;
year: string;
}
let data = [{ year: "yyyy" }, { month: "mm" }, { day: "dd" }];
const updateData = (): IData => {
const updatedData = data.reduce((accu, curr: { [x: string]: any }) => {
for (const [key, val] of curr) {
accu[key] = val;
}
return accu;
}, {});
console.log(updatedData);
return updatedData;
};
return <div className="App"></div>;
}
Using any in TypeScript defeats the purpose of using TypeScript, because you lose type safety. Best to avoid it whenever possible - then you'll be able to more reliably debug things at compile-time instead of at runtime. Here, the values of the properties are strings, so you can use that instead. Or, you could leave out the explicit annotation entirely and TypeScript will infer it just fine on its own.
While iterating, each object being iterated over is in the curr parameter. It's not an array; it's a plain object like { year: "yyyy" }. So, trying to iterate over the object with for..of doesn't make sense. Use Object.entries to iterate over each key and each value.
for (const [key, val] of Object.entries(curr)) {
But the returned object still won't be typed properly. While you could call .reduce with a generic to indicate the type of the accumulator, because the accumulator isn't fully constructed until the final iteration, you'd have to use Partial - and then you'd have to cast it back to the full IData at the end. A problem is that all approaches to iterate over the keys of an object will result in the key being typed as a string, even if the object's keys are more specific (like year | month | day) - so some sort of casting will be necessary somewhere (or you'll have to explicitly check against every possible key at runtime).
Rather than wrestling with the typing of .reduce, consider just doing:
const updateData = () => Object.assign({}, ...data) as IData;
If you really had to use .reduce, it'd look something like:
interface IData {
day: string;
month: string;
year: string;
}
let data = [{ year: "yyyy" }, { month: "mm" }, { day: "dd" }] as const;
const updateData = (): IData => {
const updatedData = data.reduce<Partial<IData>>((accu, curr) => {
for (const [key, val] of Object.entries(curr)) {
if (key !== 'year' && key !== 'month' && key !== 'day') {
throw new Error('Non-matching key found');
}
// eslint-disable-next-line no-param-reassign
accu[key] = val;
}
return accu;
}, {});
return updatedData as IData;
};

How to form flexible expandable filter with TypeScript?

I need to build a universal filter with TypeScript using operators 'OR' and 'AND'. There should be a way to use range and multiselect filters as well.
But I'm new to OOP and can't grasp how I should do it using classes.
The filter request to backend should look something like this and there has to be a way to expand them if there is a new type:
[
{ field: 'id', operator: '=', value: 12 },
'OR',
{ field: 'name', operator: '=', value: 'testName' },
'OR',
[
{ field: 'age', operator: '>=', value: '20' },
'AND',
{ field: 'age', operator: '=<', value: '30' }
]
]
I form an array of objects looking like this and feed to a class that forms filters:
[
{
field: 'id',
type: 'default',
value: 12
},
{
field: 'id2',
type: 'default',
value: 13
},
{
field: 'name',
type: 'default',
value: 'yulya'
},
{
field: 'age',
type: 'range',
value: '20 - 30'
}
]
The class FilterParameters looks like this, however, it is not expandable:
export class FilterParameters {
filter: (string | object | number)[]
constructor() {
this.filter = []
}
createFilter(receivedFields: (object)[]) {
let filterArr:(object)[] = []
let defaultFilter = receivedFields.filter((item) => item.type === 'default')
let rangeFilter = receivedFields.filter(item => item.type === 'range')
defaultFilter.forEach((item:any) => {
let defaultFilterObj = {
field: item.field,
operator: '=',
value: item.value
}
filterArr.push(defaultFilterObj)
})
rangeFilter.forEach((item:any) => {
let conditionArr = []
let startCondition = item.value.split(' ')[0]
let endCondition = item.value.split(' ').pop()
let objStartCondition = {
field: item.field,
operator: '>=',
value: startCondition
}
let objEndCondition = {
field: item.field,
operator: '=<',
value: endCondition
}
conditionArr.push(objStartCondition)
conditionArr.push('AND')
conditionArr.push(objEndCondition)
filterArr.push(conditionArr)
})
this.filter = filterArr.map((e, i) => i < filterArr.length - 1 ? [e, 'OR'] : [e]).reduce((a, b) => a.concat(b))
return this.filter
}
}
How can I create a class that forms filters and easily expandable?
There are a lot of things that I would change if I were designing this from the ground up as your current approach has a number of limitations. For example the input array always uses 'OR' with no way to use 'AND' or create groupings.
I am assuming that your input format and output format are fixed and our task is just to create a more extensible way to map from the input to the output. The first thing that I do is define those types.
Each filter of the input looks like this, and we expect to receive an array of them.
interface InputFilter {
field: string;
type: string;
value: any;
}
The output is more complicated due to the nesting.
interface OutputFilterElement {
field: string;
operator: string;
value: any;
}
type Joiner = 'OR' | 'AND';
type OutputFilterArray = Array<OutputFilterElement | Joiner | OutputFilterArray>
type OutputFilter = OutputFilterElement | OutputFilterArray;
We want parsing to be an extensible system that allows for adding new input types and mapping them to their outputs. If we want to add a new type, what information do we need? We need to know the type string that it matches and how it maps input objects of this type to outputs. We can write that as an interface.
interface TypeParser {
type: string;
parse: (input: InputFilter) => OutputFilter;
}
We want our FilterParameters class to just know about the TypeParser interface and to not know or care what concrete implementations of that interface exist. So we can instantiate this class by passing any number of TypeParser objects as an argument to the constructor.
The class has a public method parse to parse an array of input filters based on its available type parsers. It looks for the matching parser and then delegates the actual parsing.
export class FilterParameters {
private readonly typeParsers: TypeParser[];
/**
* construct an isntance of the FilterParser by passing the TypeParsers to handle specific types
*/
constructor(...typeParsers: TypeParser[]) {
this.typeParsers = typeParsers;
}
/**
* helper function parses each element of the array
* might throw an error
*/
private parseOne(input: InputFilter): OutputFilter {
// find the parser for this type
const parser = this.typeParsers.find(p => p.type === input.type);
if (!parser) {
throw new Error(`no filter found for type ${input.type}`);
}
return parser.parse(input);
}
/**
* handle an array of individual filters by assuming an 'OR' relationship
*/
public parse(input: InputFilter[]): OutputFilter {
// use flatMap to insert 'OR' between elements
return input.flatMap(
(condition, i) => {
const parsed = this.parseOne(condition);
return i === 0 ? [parsed] : ['OR', parsed]
}
);
// do we want to catch errors here? or allow them to be thrown?
}
}
I don't think it makes sense to use classes if they don't need any sort of instance so I am creating our concrete implementations of TypeParser as objects which fulfill the interface.
const EqualityParser: TypeParser = {
type: 'default',
parse: ({ field, value }: InputFilter): OutputFilter => {
return ({
field,
// operator is always equals for default filter
operator: '=',
value
});
}
}
const RangeParser: TypeParser = {
type: 'range',
parse: ({ field, value }: InputFilter): OutputFilter => {
// split based on hyphen with optional spaces before and after
const [min, max] = value.split(/\s*-\s*/);
// are these always numbers? should we use parseFloat?
return ([{
field,
operator: '>=',
value: min
},
'AND',
{
field,
operator: '<=',
value: max
}]);
}
}
In order to parse your input using these two parsers, you would write:
const filterer = new FilterParameters(EqualityParser, RangeParser);
const output = filterer.parse(input);
Typescript Playground Link

Type 'string | number | boolean' is not assignable to type 'undefined'. Type 'string' is not assignable to type 'undefined'.ts(2322)

I'm trying to create a partial object that only has certain fields of the full object that meet a criteria. However, I get the subject typescript error message when I try to assign the property. I created a test module to illustrate the concept/problem. Note that this is only to illustrate the problem. It is not the actual code.
type FullObject = {
id: number
name: string
active: boolean
}
type PartialObject = Partial<FullObject>
const myFullObj: FullObject = {
id: 1,
name: 'First Object',
active: true,
}
const myPartialObj: PartialObject = {}
let k: keyof PartialObject
for (k in myFullObj) {
if (myFullObj[k] !== undefined) myPartialObj[k] = myFullObj[k] // Error here
if (k === 'name') myPartialObj[k] = myFullObj[k] // No error here
}
Note that it is only the first "if" statement that has the error. After some research and trying various things, I worked around the problem by initializing the partial object to the full object and then deleting properties that did not meet a criteria. Since this is a backwards way of solving the problem, I would prefer to create the partial object with properties that meet criteria.
I came up with the following solution. It clearly illustrates what I am trying to do: if a criteria is met with a source object property, then copy that property into the partial destination object. In this example I'm using "not undefined" as the criteria. In the real code the criteria is more complex.
type FullObject = {
id: number
name: string
active: boolean
}
type PartialObject = Partial<FullObject>
const myFullObj: FullObject = {
id: 1,
name: 'First Object',
active: true,
}
let myPartialObj: PartialObject = {}
let k: keyof PartialObject
for (k in myFullObj) {
if (myFullObj[k] !== undefined) myPartialObj = { ...myPartialObj, ...Object.fromEntries([[k, myFullObj[k]]]) }
}
console.log(JSON.stringify(myPartialObj, null, ' '))
It seems that there must be a better way to accomplish this. However, the example illustrates what is intended.

flowtype cannot return object literal because object type is incompatible

I have the following types:
export type Transaction = {
amount: number,
description: string
}
export type TransactionsForMonth = {
month: number,
year: number,
transactions: Array<Transaction>
}
export type TransactionsGroupedByDay = Array<{
date: string,
transactionsForDay: Array<Transaction>
}>
export type TransactionsForMonthGroupedByDay = {
month: number,
year: number,
transactionsForMonth: Array<TransactionsGroupedByDay>,
}
I've created a function that converts from TransactionsForMonth to TransactionsForMonthGroupedByDay, by reducing over the transactions and sorted them into days:
transactionsGroupedByDay ({ month, year, transactions }: TransactionsForMonth): TransactionsForMonthGroupedByDay {
// transactions are already sorted in date order
const transactionsGroupedByDay: TransactionsGroupedByDay = transactions.reduce((transactionsGroupedByDay, transaction) => {
const thisDay = transactionsGroupedByDay.find(({ date }) => date === transaction.transactionDate)
const transactionsThisDay = thisDay ? [...thisDay.transactionsForDay, transaction] : [transaction]
return [
...transactionsGroupedByDay.filter(({ date }) => date !== transaction.transactionDate),
{
date: transaction.transactionDate,
transactionsForDay: transactionsThisDay
}
]
}, [])
return {
month,
year,
transactionsForMonth: transactionsGroupedByDay
}
}
When I run flow over this function, I get the following error:
Cannot return object literal because object type [1] is incompatible with TransactionsGroupedByDay [2] in array element
of property transactionsForMonth. [incompatible-return]
src/api/services/TransactionService.js
55│ return {
56│ month,
57│ year,
58│ transactionsForMonth: transactionsGroupedByDay
59│ }
60│ }
61│
src/api/types.js
[1] 35│ export type TransactionsGroupedByDay = Array<{
36│ date: string,
37│ transactionsForDay: Array<Transaction>
38│ }>
:
[2] 43│ transactionsForMonth: Array<TransactionsGroupedByDay>,
I am new to flowtype and am wondering what this error means?
The error is basically saying, the function is supposed to return an object of a given type structure, but your return statement doesn't satisfy that requirement.
The reason for that is because in type TransactionsForMonthGroupedByDay you've declared that the property transactionsForMonth is an array of TransactionsGroupedByDay but the variable you actually pass into transactionsForMonth you've declared as const transactionsGroupedByDay: TransactionsGroupedByDay which is incompatible.
You can fix this by either fixing your type to accept a single TransactionsGroupedByDay or your variable declaration needs to return an array of TransactionsGroupedByDay.

Flow: Inference error in for-of loop

I have a code this is trying to validate object with attributes id and _name for specified type. Attribute id should be a number and name should be a string as declared in FooT type.
function number(value: mixed): number {
if (typeof value === "number") return value
throw new TypeError("number required")
}
function string(value: mixed): string {
if (typeof value === "string") return value
throw new TypeError("string required")
}
function objectOf(attrs, value) {
const obj = {}
for (const key of Object.keys(attrs)) {
const typeFn = attrs[key]
obj[key] = typeFn(value[key])
}
return obj
}
type FooT = {
id: number,
name: string
}
const fooT: FooT = objectOf(
{
id: number,
name: string
},
{
id: 1,
name: "Foo"
}
)
Running flow shows this error. For some reason inferred return type of typeFn is not correctly determined in for-of loop when accessing object attribute values dynamically.
Cannot assign objectOf(...) to fooT because:
• string [1] is incompatible with number [2] in property id.
• number [3] is incompatible with string [4] in property name.
[3] 3│ function number(value: mixed): number {
:
[1] 8│ function string(value: mixed): string {
:
[2] 22│ id: number,
[4] 23│ name: string
24│ }
25│
26│ const fooT: FooT = objectOf(
27│ {
28│ id: number,
29│ name: string
30│ },
31│ {
32│ id: 1,
33│ name: "Foo"
34│ }
35│ )
36│
Is this an issue with flow or am I missing something?
It looks like you are running into issue #935 Type of object value not inferred correctly in for-in loop. You should be able to use the suppress_comment config and just put $FlowFixMe in the code to tell Flow to ignore that.

Categories

Resources