TypeScript shared configuration object with typehinting - node.js

I am developing an API library and I am curious about how should endpoint configuration problem should be approached in Node.js with TypeScript. I want all endpoint configuration to be contained within one entity.
I currently have this approach in place:
ApiConstants.ts
------------------------
const BASE_DOMAIN = 'https://api.example.com';
export default Object.freeze({
BASE_DOMAIN: {
V1: `${BASE_DOMAIN}/v0.1`,
V2: `${BASE_DOMAIN}/v0.2`,
V3: `${BASE_DOMAIN}/v0.3`,
},
PATH: {
CATS: '/animals/cats',
},
});
It does the job, I can use it in any class by importing it and accessing the values. The problem is that I want to restrict functions to only accept values declared within this object. When request constructing function should display invalid type intellisense when value is passed which is not a part of this object.
Desired type would look something like this. Path must be declared within ApiConstants.PATH object.
function makeRequest(path: ApiConstants.PATH) {
...
}
How can such behavior be achieved?

function makeRequest(path: keyof typeof ApiConstants.PATH) {
// ...
}
See in Playground
In general, you probably want to do something like
const ApiConstants = Object.freeze({
BASE_DOMAIN: {
V1: `${BASE_DOMAIN}/v0.1`,
V2: `${BASE_DOMAIN}/v0.2`,
V3: `${BASE_DOMAIN}/v0.3`,
},
PATH: {
CATS: '/animals/cats',
},
});
type ApiConstants = typeof ApiConstants;
export default ApiConstants;
to avoid having to use typeof ApiConstants all the time. Note that this means ApiConstants is both a value and a type—Typescript is OK with this, and will know from context what you’re doing, but some programmers find it confusing. A common naming convention is to use an initial lowercase letter for the value, and an initial uppercase for the type, as in const apiConstants = /*…*/; and type ApiConstants = typeof apiConstants;.
On the other hand, it’s kind of convenient that your default export is both the value and the type.
You might also want to add
export type ApiPaths = keyof ApiConstants['PATH'];
We use ['PATH'] because we’re using the type ApiConstants here, not the value. We could still use the value but we’d have to add typeof to it again, as in keyof typeof ApiConstants.PATH.

Related

A strange mismatch not noticed by Typescript. How is this possible?

Apparently, Typescript doesn't seem to recognize the difference between an object like {} and a generic array [] by accepting last one as input of a function that requires an object with {}'s structure.
To resume my problem, this is a simplified example to replicate it:
type test = { [key: string]: any };
let x: test = ["x", "y", "z"];
Actually, Typescript seems to accept this. How is this possible?
Note: The situation I ran into is more similar to this:
type fooType = { [key: string]: any };
const fooFunction<T extends fooType>(input: T) => // code...
fooFunction([]); // No red underline
But you can consider the first example. It's the same.
The main idea is to create a function that accepts only objects with a key (type string) and a value of any type.
Thank you in advance for the answers!
Differentiating between plain objects and other things (like arrays, or even functions) can be frustrating in JavaScript (and therefore Typescript).
Since an array is an object, you need a type that excludes arrays. For completeness, you may also want to exclude other non-plain objects, like functions, dates, regexes, etc, but I'll just focus on arrays.
Using your example, here are some approaches:
1. Exclude objects with numeric indexes
function fooFunction<T extends {
[key: string]: any,
[index: number]: never
}>(input: T) { }
fooFunction(['']); // Will have red underline!
fooFunction([]); // This will NOT have an underline!
In the above case, we're saying that T cannot have any numeric indexes. There is an edge case, though: an empty array has type never[], which also has no numeric indexes!
2. Exclude array-specific fields
Another approach is to identify some property common to arrays that won't be in any of the objects you plan to pass through your function:
function fooFunction<T extends {
map?: never,
}>(input: T) { }
fooFunction(['']); // Will have red underline!
fooFunction([]); // So will this!
3. Narrow the parameter type
The cleanest approach is to narrow your generic at the parameter to exclude arrays. The following example uses a utility type that returns never for lots of non-plain-object inputs (but not all of them):
type FancyObject = any[]|Function|Date|RegExp|Error
type PlainObject<T> = T extends FancyObject
? never
: T extends { [key: string]: any }
? T
: never;
function fooFunction<T>(input: PlainObject<T>) {}
fooFunction(['']); // Will have red underline!
fooFunction([]); // So will this!
fooFunction({ hello: 'world' }) // This is fine!

Wrap a problematic npm package, while maintaining type information

The Problem
We're authoring an npm package containing React components that will be used in various (internal) web sites. There is a problematic npm package dependency that we are forced to use in our react .tsx files, that has these problems:
It doesn't expose any useful types despite having .d.ts files in it... they're empty.
It tries to run when required or imported server-side, instead of waiting until called, so we have to avoid a top-level import and instead do if (window) { const module = require('package-name') } and then use it inside that block only.
It is a frequent source of errors so everything in that library needs to be run inside of a try ... catch block.
Well, At Least We Have Types
We have already created our own types file which addressed problem #1:
// problematic-package-types.d.ts
declare module 'problematic-package' {
function doErrorProneButNecessaryThing(
foo: Record<string, unknown>,
bar: string
): void
}
The Needed Solution
The long term solution is to fix this problematic library and we're looking into how to get that done (but it's not in our direct control).
In the short term, though, we need a solution now.
Note that we are configuring dynamic requires in our npm package bundler to import them only at use-time, not treating them like other imports/requires. As our package is consumed inside other applications, we don't have full control over how that application bundling works or when the components are required, so our components may end up being required server-side when they shouldn't, and we have to tolerate that. We're still learning about some aspects of this.
My Wild (But Failed) Stab
My goal is to do something more DRY like this, where we solve all three problems of strong typing, detecting server-side execution & doing nothing, and adding error handling:
// hoping to leverage our module declaration above without importing anything
import type * as ProblematicPackage from 'problematic-package'
import wrapProblematicRequire from '../utils/my-sanity-preserving-module'
const wrappedProblematicPackage = wrapProblematicRequire<ProblematicPackage>()
// then later...
const foo: Record<string, unknown> = { property1: 'yes', property2: false }
const bar = 'yodeling'
wrappedProblematicPackage.invokeIfWindowReady(
'doErrorProneButNecessaryThing',
foo,
bar
)
However, TypeScript doesn't like the import type which unfortunately makes sense:
Cannot use namespace 'ProblematicPackage' as a type.
The Plea
How do I get the type information we've placed into problematic-package-types.d.ts to use as desired?
Or ANYTHING else. Honestly, I'm open to whatever, no matter how crude or hacky, so long as we get some clarity and reliability at call sites, with full type information as described. Suggestions/advice?
Full Details
Here is the full implementation of the wrapProblematicRequire function. I haven't tested it. It's probably awful. I'm sure it could be far better but I don't have time to get this helper module super clean right now. (My attempt to handle function type information isn't quite right.)
type Func = (...args: any[]) => any
type FunctionNames<T, TName extends keyof T> = T[TName] extends Func ? TName : never
type FunctionNamesOf<T> = FunctionNames<T, keyof T>
const wrapProblematicRequire = <T>(packageName: string) => ({
invokeIfWindowReady<TName extends FunctionNamesOf<T>>(
name: T[TName] extends Func ? TName : never,
...args: T[TName] extends Func ? Parameters<T[TName]> : never
): T[TName] extends Func ? ReturnType<T[TName]> : never {
if (!window) {
// #ts-ignore
return undefined
}
try {
// #ts-ignore
return require(packageName)[name] as T[TName](...args)
} catch (error: unknown) {
// ToDo: Log errors
// #ts-ignore
return undefined
}
}
})
export default wrapProblematicRequire
P.S. await import('problematic-package') didn't seem to work. Yes, problems abound.
Cannot use namespace 'ProblematicPackage' as a type.
Well, you can get the typeof that namespace, which seems to be what you want.
To test this, I setup the following:
// problem.js
export function doErrorProneButNecessaryThing(n) {
return n;
}
export function doErrorProneButNecessaryThing2(s) {
return s;
}
console.log('did side effect');
// problem.d.ts
export function doErrorProneButNecessaryThing(n: number): number;
export function doErrorProneButNecessaryThing2(s: string): string;
And now you can do:
import type * as ProblemNs from './problem';
type Problem = typeof ProblemNs;
// works
type A = Problem['doErrorProneButNecessaryThing'] // type A = (n: number) => number
Then the wrapProblematicRequire function just takes the name of the function as a generic, pulls the args for it, and pulls the return type.
const wrapProblematicRequire = <TName extends FunctionNamesOf<Problem>>(
name: TName,
...args: Parameters<Problem[TName]>
): ReturnType<Problem[TName]> | undefined => {
if (!window) return;
const problem = require('./problem'); // type is any, but types are enforced above
try {
return problem[name](...args);
} catch (err) {
console.log('error!');
}
};
Here require('./problem') returns the any type, but the generics keep everything key safe as long as typeof ProblemNs can be trusted.
Now to test that:
console.log('start');
const result: number = wrapProblematicRequire(
'doErrorProneButNecessaryThing',
123
);
console.log('end');
Which logs:
start
did side effect
end
Which seems to work!
Codesandbox

Remove propertis not present in interface with nodejs typescript

I'm using node.js with typescript and I have a question
It is possible to remove properties that are not present in an interface when casting an object.
interface IFooReal {
prop1: string;
}
const objTMP = {prop1: 'foo001', prop2: 'foo002', prop3: 'foo003'};
cont barr = objTMP as IFooReal; //or do someting to remove properties not presente in interface
console.log(barr);
When execute console.log(barr); the result is {prop1: 'foo001'}
Using a type assertion like objTMP as IFooReal doesn't change anything about objTMP, it only tells TypeScript "please treat this as an IFooReal".
TypeScript's purpose is just annotating your code in a way that lets it check it for type safety. It doesn't modify your code, or add any execution of its own (with limited exceptions, like enums). When TypeScript is compiled into JavaScript, the process is mostly just removing those annotations.
If you want to remove all but a subset of properties from an object, then you will need to iterate through its properties and remove any that don't match the properties you want to keep. Something like this, for example:
interface IFooReal {
prop1: string;
}
const objTMP = {
prop1: 'foo001',
prop2: 'foo002',
prop3: 'foo003'
};
// You may want to get this by calling Object.keys() on another object?
const propertiesToKeep = ['prop1'];
for (let prop in objTMP) {
if (propertiesToKeep.includes(prop) === false) {
delete (objTMP as {[key:string]: any})[prop];
}
}
console.log(objTMP);
TypeScript playground
I've had to use the type assertion objTMP as {[key: string]: any}) here because otherwise TypeScript would complain that you're attempting to access a property on objTMP using a variable with type string, when TypeScript has implicitly typed it as { prop1: string, prop2: string, prop3: string } so only values of type 'prop1'|'prop2'|'prop3' can be used to access its properties.
That type assertion basically just tells TypeScript "let me try to access properties on this object using any string as the key, and don't worry about what type that property has".

Enum attribute in lit/lit-element

We are trying to build a component with a property variant that should only be set to "primary" or "secondary" (enum). Currently, we are just declaring the attribute as a String, but we were wondering if there is a better way for handling enums? For example, should we validate somehow that the current value is part of the enum? Should we throw an error if not?
I asked this question on Slack and the answers I got lean towards declaring the property as String and use hasChanged() to display a warning in the console if the property value is invalid.
Standard HTML elements accept any string as attribute values and don't throw exceptions, so web components should probably behave the same way.
This all sounds reasonable to me.
If you're using TypeScript I'd recommend just using strings. You can use export type MyEnum = 'primary' | 'secondary' to declare it and then use #property() fooBar: MyEnum to get build time checking. You can use #ts-check to do this in plain JS with #type MyEnum too.
This works well if the enums are for component options or that map to server-side enums that will get validated again.
However, if you want to validate user input into enums or loop through them a lot this is less good. As the JS runs it has no visibility of the type. You need an object dictionary, something like:
const MyEnum = Object.freeze({
primary: 'primary',
secondary: 'secondary'
});
// Enforce type in TS
const value: keyof MyEnum;
// Validate
const validated = MyEnum[input.toLower()];
// Loop
for(const enumVal of Object.keys(MyEnum)) ...
// Or Convert to a different value type
const MyEnum = Object.freeze({
primary: 1,
secondary: 2
});
These are somewhat idiosyncratic. Again, if you're using TypeScript it has an enum keyword that compiles to something like this and I'd use that rather than rolling your own. Strings are the better option unless you need to validate, loop or convert the values.

TS: Cannot invoke an expression whose type lacks a call signature when defined dynamically, but it works

I'm still quite new to typescript, so please be gentle with me if I'm doing something with no sense for this technology!
The problem that I'm trying to solve is having a dynamic way to define how my application errors should be structured, but leaving to the users the faculty to enrich the messages.
So I tried to create this logic in a module that could be extended easily from the application, but I'm currently facing the problem:
Error:(35, 18) TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'ErrorMessage' has no compatible call signatures.
What I thought it was a good idea (but please tell me if I'm wrong), was to use a register and a map to have the possibility to extend this mapping every time I want. So I created my ErrorMessage interface to be like the following:
export interface ErrorMessage {
actionMessage: string;
actionSubject: string;
originalErrorMessage?: string;
toString: () => string;
}
and a register for these, called ErrorResponseRegister, as it follows:
export enum defaultErrors {
ExceptionA = 'ExceptionA',
ExceptionB = 'ExceptionB',
}
export class ErrorResponseRegister {
private mapping: Map<string, ErrorMessage>;
constructor() {
this.mapping = new Map()
.set(defaultErrors.ExceptionA, exceptionAErrorMessage)
.set(defaultErrors.ExceptionB, exceptionBErrorMessage);
}
}
So at the end, every ErrorMessage function should look like:
export function exceptionAErrorMessage(originalErrorMessage?: string): ErrorMessage {
return {
enrichment1: "Something happened",
enrichment2: "in the application core",
originalErrorMessage: originalErrorMessage,
toString(): string {
return `${this.enrichment1} ${this.enrichment2}. Original error message: ${originalErrorMessage}`;
},
};
}
Please note I haven't used classes for this ones, as it doesn't really need to be instantiated
and I can have a bunch of them where the toString() method can vary. I just want to enforce the errors should have an enrichment1 and enrichment2 that highlight the problem in a better way for not-technical people.
So, now, back to code. When I'm trying to use the exceptionAErrorMessage statically, I can't see any problem:
console.log(exceptionAErrorMessage(originalErrorMessage).toString())
But when I try dynamically, using the map defined in the ErrorResponseRegister, something weird happens:
// In ErrorResponseRegister
public buildFor(errorType: string, originalErrorMessage?: string): Error {
const errorMessageBuilder = this.mapping.get(errorType);
if (errorMessageBuilder) {
return errorMessageBuilder(originalErrorMessage).toString();
}
return "undefined - do something else";
}
The code works as expected, the error returned is in the right format, so the toString function is executed correctly.
BUT, the following error appears in the IDE:
Error:(32, 18) TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'ErrorMessage' has no compatible call signatures.
The line that causes the problem is
errorMessageBuilder(originalPosErrorMessage).toString()
Can someone help me to understand what I'm doing wrong?
It looks like your problem is you've mistyped mapping... it doesn't hold ErrorMessage values; it holds (x?: string)=>ErrorMessage values:
private mapping: Map<string, (x?: string) => ErrorMessage>;
What's unfortunate is that you initialize this variable via new Map().set(...) instead of the using an iterable constructor argument.
The former returns a Map<any, any> which is trivially assignable to mapping despite the mistyping. That is, you ran smack into this known issue where the standard library's typings for the no-argument Map constructor signature produces Map<any, any> which suppresses all kinds of otherwise useful error messages. Perhaps that will be fixed one day, but for now I'd suggest instead that you use the iterable constructor argument, whose type signature declaration will infer reasonable types for the keys/values:
constructor() {
this.mapping = new Map([
[defaultErrors.ExceptionA, exceptionAErrorMessage],
[defaultErrors.ExceptionB, exceptionBErrorMessage]
]); // inferred as Map<defaultErrors, (orig?: string)=>ErrorMessage>
}
If you had done so, it would have flagged the assignment as an error with your original typing for mapping (e.g., Type 'Map<defaultErrors, (originalErrorMessage?: string | undefined) => ErrorMessage>' is not assignable to type 'Map<string, ErrorMessage>'.) Oh well!
Once you make those changes, things should behave more reasonably for you. Hope that helps; good luck!
Link to code

Resources