Reference imported object based on env - node.js

In my TypeScript Node app I wish to reference the exported object that matches my NODE_ENV variable.
config.ts
const test: { [index: string]: any } = {
param1: "x",
param2: {
name: "John"
}
}
const dev: { [index: string]: any } = {
param1: "y",
param2: {
name: "Mary"
}
}
export { test, dev }
main.ts
const environment = process.env.NODE_ENV || "development";
import * as config from "./config.ts";
const envConfig = config[environment]; //gives error Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'typeof import("/path_to_config.ts")'.ts(7053)

Just make the implicit any explicit:
const envConfig: any = (config as any)[environment];
This error often arises when you try to access a property of an object via ['propertyName'] instead of .propertyName, since that form bypasses TypeScript's type checking in many cases.

You could do a bit better than any by defining a type which is constrained to all possible values (which you could export from your config.tsx) e.g.
type configType ='test' | 'dev'
const envConfig = config[environment as configType];

Related

Incompatible types due to readonly

I have a object containing a middleware property but cant get the types to work. It tells me that the two middlewares are incompatible because one of them is readonly. Is there a way to solve this? - thanks :)
Example
type Middleware = (...args: unknown[]) => unknown;
type Router = { middleware?: Middleware[] };
const router = {
middleware: [() => null],
} as const satisfies Router;
Error
type Router = {
middleware?: Middleware[] | undefined;
}
Type '{ readonly middleware: readonly [() => null]; }' does not satisfy the expected type 'Router'.
Types of property 'middleware' are incompatible.
The type 'readonly [() => null]' is 'readonly' and cannot be assigned to the mutable type 'Middleware[]'.
A quick solution would be to remove the as const, which makes the object literals readonly:
const router = {
middleware: [() => null],
} satisfies Router;
Depending on your use case: you can also change the type Router by adding the utility type Readonly:
type Router = { middleware?: Readonly<Middleware[]> };
Though you are not able to call e.g. push on router.middleware.

interfaces in typescript: use function parameter on a nested object reference

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

Augment an interface to remove the indexer

In #types/node the NodeJS.ProcessEnv interface is declared with an indexer:
interface ProcessEnv {
[key: string]: string | undefined;
}
And I'm augmenting it with my defined properties:
declare module NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production';
}
}
It successfully type-checks the value of process.env.NODE_ENV, but it still allows any property. If I use the wrong property name (e.g. MODE_ENB), it doesn't produce an error, because of the indexer.
Is there a way to apply module augmentation to an interface that effectively removes an indexer?
Failed Attempts:
[key: string]: never;
NODE_ENV: 'development' | 'production';
Error: Property 'NODE_ENV' of type '"development" | "production"' is not assignable to string index type 'never'.ts(2411)
[key: Exclude<string, 'NODE_ENV'>]: never;
NODE_ENV: 'development' | 'production';
Error: An index signature parameter type cannot be a type alias. Consider writing '[key: string]: never' instead.ts(1336)
A partial solution is to use:
[key: string]: unknown;
Which will prevent other properties from being assigned to known types:
const env: string = process.env.NODE_ENVtypo;
// Type 'unknown' is not assignable to type 'string'. ts(2322)
But will still preserve the type of your defined properties:
const env: string = process.env.NODE_ENV;
But it won't catch when comparing values:
if (process.env.NODE_ENVZZZ === 'production') { // no error appears

Use commander in typescript

I try to use commander in typescript and I could like to give a proper type to my cli. So I start with this code:
import * as program from "commander";
const cli = program
.version("1.0.0")
.usage("[options]")
.option("-d, --debug", "activate more debug messages. Can be set by env var DEBUG.", false)
.parse(process.argv);
console.log(cli.debug)
But I get this error:
example.ts(9,17): error TS2339: Property 'debug' does not exist on type 'Command'.
So I tried to add an interface, as documented here:
import * as program from "commander";
interface InterfaceCLI extends commander.Command {
debug?: boolean;
}
const cli: InterfaceCLI = program
.version("1.0.0")
.usage("[options]")
.option("-d, --debug", "activate more debug messages. Can be set by env var DEBUG.", false)
.parse(process.argv);
console.log(cli.debug)
and I get this error:
example.ts(3,32): error TS2503: Cannot find namespace 'commander'.
From what I understand, cli is actually a class of type commander.Command So I tried to add a class:
import * as program from "commander";
class Cli extends program.Command {
public debug: boolean;
}
const cli: Cli = program
.version("1.0.0")
.usage("[options]")
.option("-d, --debug", "activate more debug messages. Can be set by env var DEBUG.", false)
.parse(process.argv);
console.log(cli.debug)
Which gives me this error:
example.ts(7,7): error TS2322: Type 'Command' is not assignable to type 'Cli'.
Property 'debug' is missing in type 'Command'.
I don't know how to add a property to the Command class, either in my file or in a new .d.ts file.
With your first code snippet and the following dependencies, I do not get an error:
"dependencies": {
"commander": "^2.11.0"
},
"devDependencies": {
"#types/commander": "^2.9.1",
"typescript": "^2.4.1"
}
Typescript interprets cli.debug as any. I guess the type declarations have been updated. So, if you are fine with any, the problem is solved.
If you really want to tell Typescript the type of debug, declaration merging would in principle be the way to go. It basically works like this:
class C {
public foo: number;
}
interface C {
bar: number;
}
const c = new C();
const fooBar = c.foo + c.bar;
However, there is a problem: program.Command is not a type but a variable. So, you cannot do this:
interface program.Command {
debug: boolean;
}
And while you can do this:
function f1(): typeof program.Command {
return program.Command;
}
type T = typeof program.Command;
function f2(): T {
return program.Command;
}
You can neither do this:
interface typeof program.Command {
}
Nor this:
type T = typeof program.Command;
interface T {
}
I do not know whether this problem could be solved or not.
I think I found the solution, I don't really know if it's a good practice, but nontheless.
import { Command } from 'commander';
const cli = new Command();
interface InterfaceCLI{
debug?: boolean;
}
cli
.version("1.0.0")
.usage("[options]")
.option("-d, --debug", "activate more debug messages. Can be set by env var DEBUG.", false)
.parse(process.argv);
const { debug } : InterfaceCli = <InterfaceCli><unknown>cli;
console.log(debug) // here debug is your cli.debug, I just used object destructuring
In the line before the last one I'm using type casting, I'm first casting to unkown for non-overlapping types, and then, finally, casting to our interface - InterfaceCli.

TypeScript warning => TS7017: Index signature of object type implicitly has any type

I am getting the following TypeScript warning -
Index signature of object type implicitly has any type
Here is the code that cases the warning:
Object.keys(events).forEach(function (k: string) {
const ev: ISumanEvent = events[k]; // warning is for this line!!
const toStr = String(ev);
assert(ev.explanation.length > 20, ' => (collapsed).');
if (toStr !== k) {
throw new Error(' => (collapsed).');
}
});
can anyone determine from this code block why the warning shows up? I cannot figure it out.
If it helps this is the definition for ISumanEvent:
interface ISumanEvent extends String {
explanation: string,
toString: TSumanToString
}
You could add an indexer property to your interface definition:
interface ISumanEvent extends String {
explanation: string,
toString: TSumanToString,
[key: string]: string|TSumanToString|ISumanEvent;
}
which will allow you to access it by index as you do: events[k];. Also with union indexer it's better to let the compiler infer the type instead of explicitly defining it:
const ev = events[k];

Resources