Easiest way to create a Hash of Arrays (or equivalent) in Typescript? - arrays

I am looking to create a Hash of Arrays (or some equivalent structure) that allows me to collect an unknown set of properties (keyed by name) and have each property store an array of things that claimed they have said property.
const currentProperties = currentObject.getProperties();
// we can assume getProperties correctly returns an array of valid properties
currentProperties.forEach( (v) => {
  HoA[ v ].push( currentObject );
});
I want to be able to do something like the above to populate the Hash of Arrays - but how to I actually properly initialize it/do all of the TypeScript stuff? Currently I've been getting by using an enum to manually specify the possible properties that could show up, but I want to adapt it out to a structure that doesn't need to have a property list ahead of time, and can just take whatever shows up as a key.
As noted above, I understand how to solve a version of this problem if I manually specify the expected types of properties to be seen and use a bunch of
if (currentProperties.includes(Properties.exampleOne)) {
this.exampleGroupOne.push(currentObject);
}
but I want to be able to have this work with no prior knowledge of what values of properties exist.
EDIT: some clarification on what I am asking for -
The goal is to have a bunch of objects that have a getProperties() method that returns an array of zero or more attributes. I want to have a data structure that, for each attribute that exists, ends up with an array of the objects that reported that attribute. That is easy when I know the possible attributes ahead of time, but in this case, I won't. For actually acting on the attributes, I'll need a loop that is the attributes on the outer layer [the hash] and the relevant objects on the inner layer [the array]. (This is why I'm assuming HoA)
EDIT #2:
class Alice {
myProps(): string[] {
return ["Apple"];
}
}
class Bob {
myProps(): string[] {
return ["Banana"];
}
}
class Charlie {
myProps(): string[] {
return ["Apple", "Banana"];
}
}
const FruitBasket:{ [prop: string]: string} = {}
const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();
const Objects = [myAlice, myBob, myCharlie];
for (const currentObject of Objects) {
const fruits = currentObject.myProps();
fruits.forEach( (v) => { FruitBasket[v].push(currentObject);});
}
I think this is almost what I want - I am getting an error that push does not exist on type string, but at this point I think I'm just missing something basic because I've been staring at this too long.
EDIT #3:
abstract class JustSomeGuy {
myProps(): string[] {
return [];
}
myName(): string {
return '';
}
}
class Alice extends JustSomeGuy {
myProps(): string[] {
return ["Apple"];
}
myName(): string {
return 'Alice';
}
}
class Bob extends JustSomeGuy {
myProps(): string[] {
return ["Banana"];
}
myName(): string {
return 'Bob';
}
}
class Charlie extends JustSomeGuy {
myProps(): string[] {
return ["Apple", "Banana"];
}
myName(): string {
return 'Charlie';
}
}
const FruitBasket:{ [prop: string]: JustSomeGuy[]} = {}
const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();
const Objects = [myAlice, myBob, myCharlie];
for (const currentObject of Objects) {
const fruits = currentObject.myProps();
fruits.forEach( (v) => { (FruitBasket[v] ??= []).push(currentObject);});
}
for (let key in FruitBasket){
let value = FruitBasket[key];
for (let i = 0; i < value.length; i++){
console.log("In key: " + key + " the ith element [i = " + i + "] is: " + value[i].myName() );
}
}
I believe that this is what I want. Marking this as resolved.

Let's start with the types of the data structures that you described:
type ObjWithProps = {
getProperties (): string[];
};
type PropertyHolders = {
[key: string]: ObjWithProps[] | undefined;
};
// Could also be written using built-in type utilities like this:
// type PropertyHolders = Partial<Record<string, string[]>>;
The type ObjWithProps has a method which returns an array of string elements.
The type PropertyHolders is an object type that is indexed by string values (keys), and each value type is an array of ObjWithProps (if it exists, or undefined if it doesn't) — no object has a value at every possible key.
Next, let's replicate the data structures you showed in your example:
const HoA: PropertyHolders = {};
const currentObject: ObjWithProps = {
getProperties () {
return ['p1', 'p2', 'p3' /* etc. */];
}
};
const currentProperties = currentObject.getProperties();
In the code above, the currentObject has some arbitrary properties (p1, p2, p3). This is just to have reproducible example data. Your own implementation will likely be different, but the types are the same.
Finally, let's look at the part where you assign the values to the hash map:
currentProperties.forEach((v) => {
HoA[v].push(currentObject); /*
~~~~~~
Object is possibly 'undefined'.(2532) */
});
You can see that there's a compiler error where you try to access the array at the key v. Because you aren't sure that the array exists (no object has a value at every key), trying to invoke a push method on undefined would throw a runtime error. TypeScript is trying to help you prevent that case.
Instead, you can use the nullish coalescing assignment operator (??=) to ensure that the array is created (if it doesn't already exist) before pushing in a new value. This is what that refactor would look like:
currentProperties.forEach((v) => {
(HoA[v] ??= []).push(currentObject); // ok
});
Full code in TS Playground
Utility types references:
Record<Keys, Type>
Partial<Type>

Related

Generic Typescript function to check for duplicates in an array

I'm trying make a generic Typescript function to check if an array contains duplicates. For example:
interface Student {
name: string;
class: string;
};
const students: Student[] = [
{ name: 'John Smith', class: 'Science' },
{ name: 'Edward Ryan', class: 'Math' },
{ name: 'Jessica Li', class: 'Social Studies'},
{ name: 'John Smith', class: 'English'}
];
That is the data.
This is what I want to do with the data:
const registerStudents = async (students: Student[]): Promise<void> {
checkDuplicate(students, existingState); //This is the function I want to build
const response = await axios.post('/students/new', students)
existingState.push(response); //pushes newly registers students to the existing state
};
Regarding the checkDuplicate(), I want to make it a generic function, but I'm struggling with the logic.
export const checkDuplicate = <T>(items: T[], existingState: T[]): void {
//checks if the items have any duplicate names, in this case, it would be 'John Smith', and if so, throw an error
//Also checks if items have any duplicate names with the existingState of the application, and if so, throw an error
if (duplicate) {
throw new Error('contains identical information')
};
};
It's a little bit complex and I haven't been able to figure out the logic to work with typescript. Any advice on how I can implement this would be appreciated!
One reasonable way to approach this is to have checkDuplicate() take a single array items of generic type T[], and another array keysToCheck of type K[], where K is a keylike type (or union of keylike types) and where T is a type with keys in K and whose values at those keys are strings. That is, the call signature of checkDuplicate() should be
declare const checkDuplicate: <T extends Record<K, string>, K extends PropertyKey>(
items: T[],
keysToCheck: K[]
) => void;
This function should iterate over both items and keysToCheck, and if it finds an item where a property is the same string as the same property in a previous item, it should throw an error.
If you had such a function, you could write the version which accepts students and existingState, two arrays of Student objects, like this:
function checkDuplicateStudents(students: Student[], existingState: Student[]) {
checkDuplicate([...students, ...existingState], ["name", "class"]);
}
where we are spreading the students and existingState arrays into a single array to pass as items to checkDuplicate(), and since we are checking Student we are passing ["name", "class"] as keysToCheck.
Here's a possible implementation of checkDuplicate():
const checkDuplicate = <T extends Record<K, string>, K extends PropertyKey>(
items: T[],
keysToCheck: K[]
): void => {
const vals = {} as Record<K, Set<string>>;
keysToCheck.forEach(key => vals[key] = new Set());
for (let item of items) {
for (let key of keysToCheck) {
const val: string = item[key];
const valSet: Set<string> = vals[key]
if (valSet.has(val)) {
throw new Error(
'contains identical information at key "' +
key + '" with value "' + val + '"');
};
valSet.add(val);
}
}
}
The way it works is that we create an object named vals with one key for each element key of keysToCheck. Each element vals[key] is the Set of strings we have already seen for that key. Every time we see a string-valued property val with key key in any item in the items array, we check whether the set in vals[key] has val. If so, we've seen this value for this key before, so we throw an error. If not, we add it to the set.
(Note that it's possible to replace Set<string> with a plain object of the form Record<string, true | undefined>, as shown in Mimicking sets in JavaScript? , but I'm using Set here for clarity.)
Okay, let's test it against your example:
checkDuplicateStudents(students, []);
// contains identical information at key "name" with value "John Smith"
Looks good. It throws an error at runtime and properly identifies the duplicate data.
Playground link to code

Sorting an array of maps by a key in typescript?

I'm attempting to sort an array of maps by a particular key/value within the maps.
Map<string, string>[]
I get back the following error,
Cannot assign to read only property '0' of object '[object Array]''
I'm trying to make heads or tails of this error but I feel like I'm not returning the correct value somewhere. I think my code looks mostly correct. I'm more concerned I may be trying to do something that is more difficult than I realize. Here is my code. I hard-coded the key for now just try and work through the problem and that key does exists. Any insight would be great. Thanks for looking.
sortGridData(data$ : Observable<Map<string, string>[]>) : Observable<Map<string, string>[]> {
const sortedData$ = combineLatest([data$, this.sort$]).pipe(
map(([data, sort]: [Map<string, string>[], SortDescriptor[]]) => {
data.sort((item1, item2) => {
return this.compareObjects(item1, item2, 'version')
})
return data;
})
);
return sortedData$;
}
compareObjects(object1 : Map<string, string>, object2: Map<string, string>, key) {
let item1 = object1.get(key);
let item2 = object2.get(key);
const obj1 = item1.toUpperCase()
const obj2 = item2.toUpperCase()
if (obj1 < obj2) {
return -1
}
if (obj1 > obj2) {
return 1
}
return 0
}
Sort Grid data is called at another point in my component. I'm not sure that is entirely relevant.
I will assume that the data is coming from some state-management observable based library.
If my assumption is correct the issue is because you try to mutate immutable element, what i mean is that Array.sort is making changes over the existing array named data, a.k.a. it mutates it, and because the data itself is an immutable array (read-only no modification allowed) you are receiving this error.
The thing that will most-likely solve your issue is to create a copy of the data array, and after that sort the elements inside this new copy.
sortGridData(data$ : Observable<Map<string, string>[]>) : Observable<Map<string, string>[]> {
const sortedData$ = combineLatest([data$, this.sort$]).pipe(
map(([data, sort]: [Map<string, string>[], SortDescriptor[]]) => {
// here we create a new array, that can be mutated
const dataCopy = [...data];
dataCopy.sort((item1, item2) => {
return this.compareObjects(item1, item2, 'version')
})
return dataCopy;
})
);
return sortedData$;
}
If this doesn't solve your issue please provide some more context about from where is the sortGridData being called, and to what is the data$ referring to, when the function is called.

Iterate over generic enum Types Typescript

I stumbled across a problem I have which is that I need to iterate over generic enum Types.
I'm trying to get the values of the enum Type.
Typescript error in GetPaths function in the for loop parameters:
"'T' only refers to a type, but is being used as a value here"
export enum ERoutingPaths {
Home = "/",
About = "/About"
}
export enum EGamePaths {
Snake = "/Snake",
Maze = "/Maze",
}
function GetPaths<T extends ERoutingPaths | EGamePaths>():any {
let result = "";
for (let item in Object.values(T)) {
result += item + "|";
}
return result;
}
How can I make a function which allows me to iterate over a generic enum type if I would have other enum types aswell ?
Enums with string values are old plain JavaScript objects with strict key-value typing. You may use Record<string, string> type.
function GetPaths(value: Record<string, string>): string {
let result = "";
for (let item in value) {
result += value[item] + "|";
}
return result;
}
If you want to only allow a strict set of types here you may stay with generics and use: T extends typeof ERoutingPaths | typeof EGamePaths. Notice a word typeof - we have to extract a type from an enum. There is also one more change needed.
for (let item in value) {
result += value[item as keyof T] + "|";
}
TypeScript widens item type to string - compiler doesn't allow using strings with enums so I'm casting it back to keyof T.

Define Dynamic type to array in typescript

I have an class which contain array.
let object=new DynamicARRAY()
object.add(12)
object.add("google") // This should not allowed
let object=new DynamicARRAY()
object.add("google")
object.add(12) // This should not allowed
once i set type of and array how it can maintain type of that array.
i have some thing like that
class DynamicArray {
add(value:string):Array<number>
add(value:number):Array<string>
add<T>(value:T):Array<T> {
let collection = new Array<T>()
collection.push(value)
return collection
}
}
but not show how i can move collection in class level and it maintain its type.
Need just hint on right direction.
What you want is to make your class generic and then constrain you class method to use that generic type.
class DynamicArray<T> {
add(value:T):Array<T> {
let collection = new Array<T>()
collection.push(value)
return collection
}
}
When using the class you specify what type it will hold
const array = new DynamicArray<string>();
array.add('foo');
array.add(12); // will fail
The only way I can think of having a generic entry point would be a static class which instantiate a generic typed class.
class TypedArray<T> {
private items: T[] = [];
add(value: T): this {
this.items.push(value);
return this;
}
}
class DynamicArray {
static add<T>(value:T): TypedArray<T> {
let collection = new TypedArray<T>()
collection.add(value);
return collection
}
}
const array = DynamicArray.add('foo');
array.add('bar');
array.add(12); // fails

TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'rowType' [duplicate]

When using Object.keys(obj), the return value is a string[], whereas I want a (keyof obj)[].
const v = {
a: 1,
b: 2
}
Object.keys(v).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, []);
I have the error:
Element implicitly has an 'any' type because type '{ a: number; b: number; }' has no index signature.
TypeScript 3.1 with strict: true. Playground: here, please check all checkboxes in Options to activate strict: true.
Object.keys returns a string[]. This is by design as described in this issue
This is intentional. Types in TS are open ended. So keysof will likely be less than all properties you would get at runtime.
There are several solution, the simplest one is to just use a type assertion:
const v = {
a: 1,
b: 2
};
var values = (Object.keys(v) as Array<keyof typeof v>).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
You can also create an alias for keys in Object that will return the type you want:
export const v = {
a: 1,
b: 2
};
declare global {
interface ObjectConstructor {
typedKeys<T>(obj: T): Array<keyof T>
}
}
Object.typedKeys = Object.keys as any
var values = Object.typedKeys(v).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
Based on Titian Cernicova-Dragomir answer and comment
Use type assertion only if you know that your object doesn't have extra properties (such is the case for an object literal but not an object parameter).
Explicit assertion
Object.keys(obj) as Array<keyof typeof obj>
Hidden assertion
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
Use getKeys instead of Object.keys. getKeys is a ref to Object.keys, but the return is typed literally.
Discussions
One of TypeScript’s core principles is that type checking focuses on the shape that values have. (reference)
interface SimpleObject {
a: string
b: string
}
const x = {
a: "article",
b: "bridge",
c: "Camel"
}
x qualifies as a SimpleObject because it has it's shape. This means that when we see a SimpleObject, we know that it has properties a and b, but it might have additional properties as well.
const someFunction = (obj: SimpleObject) => {
Object.keys(obj).forEach((k)=>{
....
})
}
someFunction(x)
Let's see what would happen if by default we would type Object.keys as desired by the OP "literally":
We would get that typeof k is "a"|"b". When iterating the actual values would be a, b, c. Typescript protects us from such an error by typing k as a string.
Type assertion is exactly for such cases - when the programmer has additional knowledge. if you know that obj doesn't have extra properties you can use literal type assertion.
See https://github.com/microsoft/TypeScript/issues/20503.
declare const BetterObject: {
keys<T extends {}>(object: T): (keyof T)[]
}
const icons: IconName[] = BetterObject.keys(IconMap)
Will retain type of keys instead of string[]
1. npm install ts-extras (written by sindresorhus)
Use it:
import { objectKeys } from 'ts-extras'
objectKeys(yourObject)
That's it.
====
Here's another pkg I made before I knew about ts-extras:
npm install object-typed --save
import { ObjectTyped } from 'object-typed'
ObjectTyped.keys({ a: 'b' })
This will return an array of type ['a']
I completely disagree with Typescript's team's decision...
Following their logic, Object.values should always return any, as we could add more properties at run-time...
I think the proper way to go is to create interfaces with optional properties and set (or not) those properties as you go...
So I simply overwrote locally the ObjectConstructor interface, by adding a declaration file (aka: whatever.d.ts) to my project with the following content:
declare interface ObjectConstructor extends Omit<ObjectConstructor, 'keys' | 'entries'> {
/**
* Returns the names of the enumerable string properties and methods of an object.
* #param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
keys<O extends any[]>(obj: O): Array<keyof O>;
keys<O extends Record<Readonly<string>, any>>(obj: O): Array<keyof O>;
keys(obj: object): string[];
/**
* Returns an array of key/values of the enumerable properties of an object
* #param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T extends { [K: Readonly<string>]: any }>(obj: T): Array<[keyof T, T[keyof T]]>
entries<T extends object>(obj: { [s: string]: T } | ArrayLike<T>): [string, T[keyof T]][];
entries<T>(obj: { [s: string]: T } | ArrayLike<T>): [string, T][];
entries(obj: {}): [string, any][];
}
declare var Object: ObjectConstructor;
Note:
Object.keys/Object.entries of primitive types (object) will return never[] and [never, never][] instead of the normal string[] and [string, any][]. If anyone knows a solutions, please, feel free to tell me in the comments and I will edit my answer
const a: {} = {};
const b: object = {};
const c: {x:string, y:number} = { x: '', y: 2 };
// before
Object.keys(a) // string[]
Object.keys(b) // string[]
Object.keys(c) // string[]
Object.entries(a) // [string, unknown][]
Object.entries(b) // [string, any][]
Object.entries(c) // [string, string|number][]
// after
Object.keys(a) // never[]
Object.keys(b) // never[]
Object.keys(c) // ('x'|'y')[]
Object.entries(a) // [never, never][]
Object.entries(b) // [never, never][]
Object.entries(c) // ['x'|'y', string|number][]
So, use this with caution...
You can use the Extract utility type to conform your param to only the keys of obj which are strings (thus, ignoring any numbers/symbols when you are coding).
const obj = {
a: 'hello',
b: 'world',
1: 123 // 100% valid
} // if this was the literal code, you should add ` as const` assertion here
// util
type StringKeys<objType extends {}> = Array<Extract<keyof objType, string>>
// typedObjKeys will be ['a', 'b', '1'] at runtime
// ...but it's type will be Array<'a' | 'b'>
const typedObjKeys = Object.keys(obj) as StringKeys<typeof obj>
typedObjKeys.forEach((key) => {
// key's type: 'a' | 'b'
// runtime: 'a', 'b', AND '1'
const value = obj[key]
// value will be typed as just `string` when it's really `string | number`
})
All that said, most developers would probably consider having numbers as keys a poor design decision/bug to be fixed.
Here is a pattern I use for copying objects in a typesafe way. It uses string narrowing so the compiler can infer the keys are actually types. This was demonstrated with a class, but would work with/between interfaces or anonymous types of the same shape.
It is a bit verbose, but arguably more straightforward than the accepted answer. If you have to do the copying operation in multiple places, it does save typing.
Note this will throw an error if the types don't match, which you'd want, but doesn't throw an error if there are missing fields in thingNum. So this is maybe a disadvantage over Object.keys.
class thing {
a: number = 1;
b: number = 2;
}
type thingNum = 'a' | 'b';
const thingNums: thingNum[] = ['a', 'b'];
const thing1: thing = new thing();
const thing2: thing = new thing();
...
thingNums.forEach((param) => {
thing2[param] = thing1[param];
});
playground link
Here's a more accurate utility function:
const keys = Object.keys as <T>(obj: T) =>
(keyof T extends infer U ? U extends string ? U : U extends number ? `${U}` : never : never)[];
Explanation: keyof T extends string | number | symbol, however Object.keys omits the symbol keys and returns number keys as strings. We can convert number keys to string with a template literal `${U}`.
Using this keys utility:
const o = {
x: 5,
4: 6,
[Symbol('y')]: 7,
};
for(const key of keys(o)) {
// key has type 'x' | '4'
}
As a possible solution, you can iterate using for..in over your object:
for (const key in myObject) {
console.log(myObject[key].abc); // works, but `key` is still just `string`
}
While this, as you said, would not work:
for (const key of Object.keys(myObject)) {
console.log(myObject[key].abc); // doesn't!
}

Resources