I must be missing something, but I found several ways to iterate through an Enum but not on a string enum.
The following enum is given:
export enum Locales {
En = 'en',
Fr = 'fr',
De = 'de',
Es = 'es',
It = 'it',
Nl = 'nl',
No = 'no',
Tr = 'tr',
}
What I want to achieve:
I want to iterate on that string enum so that I get the values (!). What I've tried:
for (const key of Object.keys(Locales)) {
const locale: string = Locales[key];
console.log(locale); // Should print 'en', 'fr' and so on
}
The problem with above code:
Due to the strict tsconfig (which doesn't allow implicit anys) I can not compile this to javascript. Since this is not my project I can not change this tsconfig either. It highlights the key variable at Locales[key] and the error makes sense to me:
[ts] Element implicitly has an 'any' type because index expression is
not of type 'number'.
The question:
What's the proper way iterating through a string enum to get it's values with Typescript 2.6+?
As betadeveloper suggested, you can get proper type for key if you use type assertion as keyof typeof Locales. Or you can wrap it in type-safe variant of Object.keys() function like this:
export enum Locales {
En = 'en',
Fr = 'fr',
De = 'de',
Es = 'es',
It = 'it',
Nl = 'nl',
No = 'no',
Tr = 'tr',
}
function enumKeys<E>(e: E): (keyof E)[] {
return Object.keys(e) as (keyof E)[];
}
for (const key of enumKeys(Locales)) {
const locale: string = Locales[key];
console.log(locale);
}
Also, for the record, old-style for .. in loop still works:
for (let key in Locales) {
let locale = Locales[key];
console.log(locale);
}
#Artem and #betadeveloper pointed out that I can use the keyof typeof Locales type for my approach. The solution I eventually came up with looks like this:
const keys: (keyof typeof Locales)[] = <(keyof typeof Locales)[]>Object.keys(Locales);
for (const key of keys) {
const locale: string = Locales[key];
console.log(locale); // Prints 'en', 'fr' and so on
}
Lodash
Lodash is a good option to use since it is easy to use and provides a easy to understand api. From the lodash methods, forIn is the option you're looking for. To get typescript declaration files, you can install:
npm install #types/lodash
With the forIn method, you get the value and the key of Locales object.
import { forIn } from 'lodash'
enum Locales {
En = 'en',
Fr = 'fr'
// ...
}
forIn(Locales, (value, key) => console.log(value, key))
Adding to this question because I was looking for a solution.
The best solution is to use Object.values() https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
Object.values(Locales).forEach((locale) => console.log(locale));
// or
for (const locale of Object.values(Locales)) {
console.log(locale);
}
Also consider Object.entries() to get both the keys and values of a string value enum.
const entries = Object.entries(Locales);
let LocalesArray= [];
entries.forEach((type) => {
LocalesArray.push({ key: type[0], name: type[1] });
});
Simple way to iterate over enum values and convert them into object array.
Related
I have an interface with
interface mathTest {
mathAction: MathActionEnum;
}
The reason for this is that I want this property to have just one of the specific values from the enum below.
enum MathActionEnum {
'byOne' = 1,
'byTwo' = 2,
'byFour' = 4,
'byEight' = 8,
}
Assume mathAction = 'byOne' -> received from an API response.
First scenario: doing an arithmetic operation, I need the number value: let result: number = amount / MathActionEnum[mathAction] but I get an error:
The right-hand side of an arithmetic operation must be of type 'any',
'number', 'bigint' or an enum type
It is a number but still I need to cast it with Number(MathActionEnum[mathAction]) for the error to go away.
Second scenario: equality check, I need the string value: if (mathAction === MathActionEnum[MathActionEnum.byOne]) but I get an error:
This condition will always return 'false' since the types
'MathActionEnum' and 'string' have no overlap
Which makes sense.
I'm a bit lost, is there a way to syntax it as I expect it to be? Maybe I need to define things differently?
Thanks
TypeScript enums are absolutely NOT suitable for any sort of key-value mapping. The intent is to have a grouping of uniquely identifiable labels, but labels are where it ends. While they may indeed have a number representation under the hood, they are not intended for use as a key-value store. You will have to cast it to "extract the number", and then the type is just number, so you effectively defeat the purpose of enums.
For all intents and purposes, think of them like keys with no useful values:
const MathActionEnum = Object.freeze({
byOne: Symbol(),
byTwo: Symbol(),
byFour: Symbol(),
byEight: Symbol(),
})
Consider the newer alternative, const assertion, instead. They'll provide you with type safety on both keys and values:
const MathActions = {
'byOne': 1,
'byTwo': 2,
'byFour': 4,
'byEight': 8,
} as const
type MathAction = keyof typeof MathActions
type MathActionValue = typeof MathActions[MathAction]
You get full type safety on both keys and values:
const example = (action: MathAction) => {
return 2 * MathActions[action]
}
example('byOne')
// compile error, not a valid key
example('foo')
// -------------
const example2 = (actionValue: MathActionValue) => {
return 2 * actionValue
}
example2(4)
// compile error, not a valid value
example2(19)
You can even add type assertions to check if arbitrary values are a key or value:
const isAction = (action: string): action is MathAction => {
return Object.keys(MathActions).includes(action)
}
isAction
const isActionValue = (actionValue: number): actionValue is MathActionValue => {
return Object.values(MathActions).includes(actionValue as any)
}
You'll even get IDE autocompletion for both keys and values:
Here's a Playground
I have this object model:
export interface UpdateDocument {
updated_at?: string;
actions?: Actions;
}
export interface Actions {
update?: Update;
create?: Create;
}
export interface Update {
name?: Values;
priority?: Values;
engine?: Values;
fact?: Fact;
}
export interface Fact {
brand?: Values;
model?: Values;
version?: Values;
year?: Values;
km?: Values;
color?: Values;
}
export interface Values {
old?: any;
new?: any;
}
export interface Create {
conditions?: Object;
recipe?: Object;
}
In this function i tried to pass a parameter to references an objects field and do an assignment:
async buildUpdateDocument (updateDocument: UpdateDocument) {
let fields: Array<string> = ['name','priority','engine','fact','adjustment'];
fields.forEach((field: string) =>{
updateDocument.actions!.update![field]!.old = await this.getValue(field)
})
}
but i hav this ts-error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Update'.
No index signature with a parameter of type 'string' was found on type 'Update'.ts(7053)
How can i pass the parameter in this kind of reference to do the assignment?
First of you have specified a wrong key adjustment that doesn't exist on Update. This example uses a explicit type (as const):
let fields = ['name','priority','engine','fact'] as const;
Make sure to not add a type definition to the variable when using as const.
Here is the modified function to better fit TS standards. This also addresses the forEach-async problem in the original code. The real correct structure would be null checks for each of the x | undefined types, but to override the type errors the following is the way to go.
async function buildUpdateDocument (updateDocument: UpdateDocument) {
const fields: Array<keyof Update> = ['name','priority','engine','fact'];
await Promise.all(fields.map(async (field) => {
(((updateDocument.actions as {update: Update}).update)[field] as Values).old = await this.getValue(field);
}));
}
Your current code has bugs that the type system would help you find if you let it. First, the adjustment field doesn't exist on the Update type, and old field doesn't exist on the Fact type.
To implement this properly, I would use a Record for the data type instead:
const updateFields = ['name', 'priority', 'engine', 'fact'] as const
export type UpdateFields = typeof updateFields[number]
export type Update = Record<UpdateFields, Values>
And then, your function will look like this:
async buildUpdateDocument (updateDocument: UpdateDocument) {
updateFields.forEach((field) =>{
updateDocument.actions!.update![field]!.old = await this.getValue(field)
})
}
I have a config file. It has variables stored in the following manner.
[general]
webapp=/var/www
data=/home/data
[env]
WEBAPP_DEPLOY=${general:webapp}/storage/deploy
SYSTEM_DEPLOY=${general:data}/deploy
As you can see it has 2 sections general and env. Section env uses the variables from section general.
So I want to read this file into a variable. Let's say config. Here's I want config object to look like:
{
general: {
webapp: '/var/www',
data: '/home/data'
},
env: {
WEBAPP_DEPLOY: '/var/www/storage/deploy',
SYSTEM_DEPLOY: '/home/data/deploy'
}
}
I general I am looking for a config parser for nodejs that supports string interpolation.
I would assume most ini libraries don't include the variable expansion functionality, but with lodash primitives a generic "deep object replacer" isn't too complex.
I've switched the : delimiter for . so has and get can lookup values directly.
const { get, has, isPlainObject, reduce } = require('lodash')
// Match all tokens like `${a.b}` and capture the variable path inside the parens
const re_token = /\${([\w$][\w\.$]*?)}/g
// If a string includes a token and the token exists in the object, replace it
function tokenReplace(value, key, object){
if (!value || !value.replace) return value
return value.replace(re_token, (match_string, token_path) => {
if (has(object, token_path)) return get(object, token_path)
return match_string
})
}
// Deep clone any plain objects and strings, replacing tokens
function plainObjectReplacer(node, object = node){
return reduce(node, (result, value, key) => {
result[key] = (isPlainObject(value))
? plainObjectReplacer(value, object)
: tokenReplace(value, key, object)
return result
}, {})
}
> plainObjectReplacer({ a: { b: { c: 1 }}, d: 'wat', e: '${d}${a.b.c}' })
{ a: { b: { c: 1 } }, d: 'wat', e: 'wat1' }
You'll find most config management tools (like ansible) can do this sort of variable expansion for you before app runtime, at deployment.
I am using node-config in basically all my projects and most of the time I come across the problem of parsing booleans and numbers which are set as environment variables.
E.g.
default.js
module.exports = {
myNumber = 10,
myBool = true
}
custom-environment-variables.js
module.exports = {
myNumber = "MY_NUMBER",
myBool = "MY_BOOL"
}
Now, the obvious problem is that if I override the default values with custom values set as environment variables they will be a string value instead of a number or boolean value. So now, to make sure in my code that the types are correct. I always have to do type conversion and for booleans use a proper library e.g. yn. The problem is I have to do this conversion every time I use config.get() for example +config.get("myNumber") or yn(config.get("myBool")).
Is there a better and more elegant way to do this?
One solution I see would be to add a type property to an environment variable as it is done here with format. This would allow to do something like this...
custom-environment-variables.js
module.exports = {
myNumber = {
name: "MY_NUMBER",
type: "number"
},
myBool = {
name: "MY_BOOL",
type: "boolean"
}
}
node-config would handle the type conversions and there would be no need to do it all the time in the code when getting it. Of course there would be the requirement to implement a proper parser for booleans but those already exist and could be used here.
By default, environment variables will be parsed as string.
In node-config, we could override this behaviour with __format as shown below.
We don't need any additional libraries. Normal json datatypes like boolean, number, nested json etc., should work well.
Taking an easy to relate example.
config/default.json
{
"service": {
"autostart": false
}
}
custom-environment-variables.json
{
"service": {
"autostart": {
"__name": "AUTOSTART",
"__format": "json"
}
}
}
Now we can pass environment variables when we like to override and no type conversation should be needed for basic types.
This feature is now supported in node-config v3.3.2, see changelog
I use this method:
const toBoolean = (dataStr) => {
return !!(dataStr?.toLowerCase?.() === 'true' || dataStr === true);
};
You can add cases if you want 0 to resolve to true as well:
const toBoolean = (dataStr) => {
return !!(dataStr?.toLowerCase?.() === 'true' || dataStr === true || Number.parseInt(dataStr, 10) === 0);
};
Using grpc with Node, Enums in responses to my queries are resolving as integer values. However, when I make the same queries with BloomRPC, the Enums resolve as Integer values.
Is there a parameter or option to force these Enums to be resolved as String using Node grpc?
In our project, we use enum to help us ensure the integrity of a finite set of possibilities by eliminating human error. Why should we need to remember what the string value is when we have the protocol buffer enum so handy? Thus, we use the .proto as the source of truth; that's our rule.
To do that, follow these steps, which are written for ES6+ code.
Define your gRPC/Protobuf enum in a .proto file.
// life.proto
syntax = 'proto3';
package life;
enum Choices
{
EAT = 0;
DRINK = 1;
SLEEP = 2;
CODE = 3;
SKI = 4;
}
Install #grpc/proto-loader and #grpc/grpc-js.
$ npm i -s #grpc/proto-loader #grpc/grpc-js
Import the modules that pay the bills, so to speak. Load the .proto file directly into memory (don't compile).
// myNodeApp.js
import * as grpc from '#grpc/grpc-js'
import * as protoLoader from '#grpc/proto-loader'
import path from 'path'
// these options help make definitions usable in our code
const protoOptions = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
// this allows us to prepare the path to the current dir
const dir = path.dirname(new URL(import.meta.url).pathname)
// this loads the .proto file
const lifeDef = protoLoader.loadSync(path.join(dir, 'life.proto'), protoOptions)
// this loads the package 'life' from the .proto file
const life = grpc.loadPackageDefinition(lifeDef).life
Take a peek at the enum Choices definition (in the same file).
// myNodeApp.js (cont'd)
console.log(life.Choices)
/* stdout */
{
format: 'Protocol Buffer 3 EnumDescriptorProto',
type: {
value: [ [Object], [Object], [Object], [Object], [Object] ],
name: 'Choices',
options: null
},
fileDescriptorProtos: [
<Buffer 0a ... 328 more bytes>
]
}
...look deeper...
console.log(life.Choices.value)
/* stdout */
{
value: [
{ name: 'EAT', number: 0, options: null },
{ name: 'DRINK', number: 1, options: null },
{ name: 'SLEEP', number: 2, options: null },
{ name: 'CODE', number: 3, options: null },
{ name: 'SKI', number: 4, options: null }
],
name: 'Choices',
options: null
}
Use the enum.
// myNodeApp.js
const myDay = { // plain JSON (or define a gRPC message, same same)
dawn: life.Choices.type.value[1].name,
morning: life.Choices.type.value[0].name,
afternoon: life.Choices.type.value[4].name,
evening: life.Choices.type.value[3].name,
night: life.Choices.type.value[2].name
}
You could write an accessor or utility function to manage the key lookup (by passing the imported grpc enum and index), like so:
export const getEnumByName = function (protoEnum, needle) {
return protoEnum.type.value.find(p => {
return p.name === needle
})
}
export const getEnumByNum = function (protoEnum, needle) {
return protoEnum.type.value.filter(p => {
return p.number = needle
})
}
export const getEnumKeys = function (protoEnum, key = 'name') {
return protoEnum.type.value.map(p => {
return p[key]
})
}
Inverting and assigning a value to a Message is what's already covered in other answers, just set the enum field to the string value using, you guessed it, the string that represents the enum name, which you access using the code above.
This is along the lines of how we do it. Clean and simple, just a touch obscure until you look "under the hood" one day.
Learn more about #grpc/proto-loader and #grpc/grpc-js. Hope this helps someone out there in the wild. :)
If you are using the #grpc/proto-loader library, you can set the option enums to the value String (not the string "String", the constructor function String). Then all enum values will be represented by their name strings.