Iterate over generic enum Types Typescript - reactjs

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.

Related

type narrowing in ternary

In myFunction below, I accept a parameter which is either a string or an array of strings, and I normalize it to an array in the function body:
export const myFunction = <T extends string | string[]>(
myParam: T
) => {
let myStringArray = (Array.isArray(myParam) ? myParam : [myParam])
}
I expected Array.isArray to narrow the type in the branches of the ternary operator to either string or string[] so that the final type of myStringArray would be string[]. Instead, the final type is more complicated: (T & any[]) | T[].
TypeScript Playground
I noticed, however, that if I restrucure the code to use an if-else instead, the type-narrowing works perfectly:
export const myFunction = <T extends string | string[]>(
myParam: T
) => {
let myStringArray;
if (Array.isArray(myParam)) {
myStringArray = myParam;
} else {
myStringArray = [myParam];
}
console.log(myStringArray);
}
The type of myStringArray on the last line is string[], as expected.
TypeScript Playground
Is it possible to get the type-narrowing to work with the ternary operator? Are the two expressions not equivalent?
The TS compiler does not evaluate the logic of ternary expressions when producing a return type.
For a simpler example:
const x = true ? 1 : 0 // TS says 1 | 0
Despite the 0 value being impossible, TS does not discard it.
The only analysis TS performs on ternary expressions is type refinement within them:
This has been previously reported as marked as working as intended: https://github.com/microsoft/TypeScript/issues/39550

[first, ...rest]: A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)

Setting
class X {
constructor(first: string, ...rest: string[]) { }
}
new X(...["foo", "bar"])
yields the error
A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
This is working:
new X("foo", ...["bar"])
but that's not very handy.
If I use
class X {
constructor(...all: string[]) { }
}
instead it's working fine, so it must have something to do with me splitting into first and rest. But is there a way to make this work with split arguments?
The type of ["foo", "bar"] is string[]. If you can't control the type of this with as const, because you got it elsewhere, you can validate and narrow the type, for example:
const input:string[] = ["foo", "bar"];
if (!input.length<1) {
throw new Error('Input must at least have 1 element');
}
const tuple: [string, ...string[]] = [input[0], ...input.slice(1)];
}
If the assignment feels silly, I feel this is way more elegant:
const input:string[] = ["foo", "bar"];
function assertAtLeast1Element(input: string[]): asserts input is [string, ...string[]] {
if (input.length < 1) throw new Error('Input must at least have 1 element');
}
assertAtLeast1Element(input);
class X {
constructor(first: string, ...rest: string[]) { }
}
new X(...input);

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

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>

How to declare the return type of a generic arrow function to be an array and value at the same time in typescript?

Consider a function which takes an array, property name and an arrow function as arguments. What I want to achieve is, filter the array and overwrite the mentioned property's value with an array (output of the arrow function) based on the length check.
overwriteFieldWith<T1, T2 extends T1[keyof T1]>(
input: Array<T1>, property: keyof T1, onOverwrite: (i: T1) => T2
): T[] {
// filter the input array and replace the property for
// each item with the output of the supplied arrow function
return input.filter(i => {
const value = onOverwrite(i);
if(value.length) { // giving me error 'length does not exist on type T2'
i[property] = value;
console.log(i);
return true;
} else {
return false;
}
});
}
It gives me error when I try to do length check on it. The supplied arrow function is always going to return an array but how can I satisfy the compiler on this?
Edit:
Link to the ts playground.
Here's one approach:
overwriteFieldWith<T, K extends keyof T>(
input: Array<T>,
property: K,
onOverwrite: (i: T) => Extract<T[K], readonly any[]>
): T[] {
return input.filter(i => {
const value = onOverwrite(i);
if (value.length) {
i[property] = value;
return true;
} else {
return false;
}
});
}
I made the function generic in the type K of property, so that the type checker keeps track of which property we're talking about. That way we can require that onOverWrite() actually return something assignable to that property value instead of something else... we need to know this or else i[property] = value might be unsafe.
I also made the return type of onOverwrite() just Extract<T[K], readonly any[]> using the Extract<T, U> union filtering utility type instead of some generic U extends T[K].
(Note that the readonly any[] type is wider than just any[]; we don't care about modifying the array so we don't need to require mutable methods to exist).
This serves the purpose of convincing the compiler that value.length will exist (since the compiler accepts that Extract<T, U> is assignable to both T and U, so the compiler knows that value is an readonly any[]). It also serves the purpose of requiring that onOverwrite() return only those union members of the property type T[K] that are arrays. This works for your example use case at any rate.
Let's test it:
const filtered = a.overwriteFieldWith(elementTypes, 'elements', (i) =>
i?.elements?.filter(data => !data.hide) ?? []);
/* (method) A.overwriteFieldWith<ElementType, "elements">(input: ElementType[],
property: "elements", onOverwrite: (i: ElementType) => TElement[]
): ElementType[] */
Looks good. Note that the compiler inferred T as ElementType and K as "elements", and thus that the return type of onOverwrite() is TElment[] as demonstrated here:
type TK = ElementType['elements'];
//type TK = TElement[] | undefined
type XTK = Extract<ElementType['elements'], readonly any[]>
// type XTK = TElement[]
So an ElementType might have an undefined elements property, but onOverwrite cannot return undefined.
Playground link to code

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