Using `lean` on mongoose queries that return arrays in TypeScript - node.js

I have two Mongoose queries and decided it would be best to use .lean() on them.
For the one that returns a single document, it seems to work fine:
let something:Something;
SomethingDocument.findOne({_id:theId}).lean().then( (res) => { something = res;});
The problem is when I try to use it with a query that returns multiple results:
let somethings:Something[];
SomethingDocument.find({color:'blue'}).lean().then( (res) => { somethings = res;});
The second call gives the error:
Type 'Object' is not assignable to type 'Something[]'.
The 'Object' type is assignable to very few other types. Did you mean to use the 'any' type instead?
Property 'length' is missing in type 'Object'.
If I try to do a type conversion it just complains the the 'length' property is missing in type 'Object'.
How do I use lean when I expect an array of results?
...note that if I simply omit lean it all works.

Mongoose type definitions are not so good IMHO, so you can fix it using this:
let somethings:Something[];
SomethingDocument.find({color:'blue'}).lean().then((res) => { somethings = res as any;});
And by the way, I would suggest to use await if you are able to (you have to compile TS to a modern Ecma version):
const somethings = await SomethingDocument.find({color:'blue'}).lean() as Something[];
Note that the former version catches errors on .catch, but the second one will throw an exception.

Related

Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'

I am declaring the following variables using TypeScript:
const BOT_PREFIX: string = process.env.PREFIX;
const BOT_TOKEN: string = process.env.TOKEN;
I get the following error:
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.ts(2322)
I can fix it by changing the data type to any, but i don't want to do that.
Node's process.env has the following type definition (from this declaration file which uses a type defined here):
interface ProcessEnv {
[key: string]: string | undefined
}
So, as far as TypeScript's compiler is concerned, any property you access under process.env will be of type string | undefined. That's a union, meaning that process.env.RANDOMKEY (for example) will either be a string or it will be undefined. Generally speaking this is the right type; the compiler has no idea which environment variables are actually set.
And so this is a problem:
const BOT_PREFIX: string = process.env.PREFIX // error!
and the compiler warns you that process.env.PREFIX might be undefined, so it's not safe to treat it as a string.
The way to deal with this depends on whether you want convenience or type safety, and what you want to see happen if your assumptions about process.env.PREFIX and process.env.TOKEN are incorrect.
For pure convenience, you probably can't beat the non-null assertion operator (!):
const BOT_PREFIX: string = process.env.PREFIX!; // okay
const BOT_TOKEN: string = process.env.TOKEN!; // okay
Here you are just telling the compiler that, even though it cannot verify this, you are sure process.env.PREFIX and process.env.TOKEN will be defined. This essentially just suppresses the compiler warning; it's still doing the same thing at runtime as your original code. And that means if you turn out to be wrong about your assertion, then you might run into problems at runtime that the compiler can't help you with:
BOT_PREFIX.toUpperCase(); // runtime error if BOT_PREFIX is undefined after all
So be careful.
On the other hand, you can try to make the code safer by handling the situation in which the environment variables you expect are not set. For example:
function getEnvVar(v: string): string {
const ret = process.env[v];
if (ret === undefined) {
throw new Error("process.env." + v + " is undefined!");
}
return ret;
}
const BOT_PREFIX: string = getEnvVar("PREFIX");
const BOT_TOKEN: string = getEnvVar("TOKEN");
Here we've written a function called getEnvVar() which takes the name of the environment variable and returns its value, as long as that value is a string. If the environment variable is not defined, then a runtime error will be thrown. TypeScript understands via control flow analysis that the return type of getEnvVar() is string (and not string | undefined; the undefined possibility has been eliminated by the throw statement), and so you can safely assign the returned value to a variable of type string.
This code is obviously less convenient since it requires extra runtime code, but now instead of possibly lying to the compiler and having bizarre runtime behavior, you will get some immediate feedback at runtime if your assumptions are invalid.
Playground link to code
Since those are environment variables which makes it pretty sure that those value won't be empty or undefined.
You may use non-null assertion operator
should look like this:
const BOT_PREFIX: string = process.env.PREFIX!;
const BOT_TOKEN: string = process.env.TOKEN!;
Just make sure your environment variables are filled.
you can also do this:
const BOT_PREFIX: string = process.env.PREFIX || '';
const BOT_TOKEN: string = process.env.TOKEN || '';
You can also use ES6 template literals
const BOT_PREFIX: string = `${process.env.PREFIX}`;
const BOT_TOKEN: string = `${process.env.TOKEN}`;

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.

Typescript error TS2504 when trying to use async iterators with Mongoose 5.11.14: must have a '[Symbol.asyncIterator]()'

I'm trying to move away from using #types/mongoose after realising that mongoose 5.11 has type information.
However I'm now encountering Typescript issues when running tsc on the following code:
for await (const document of db.myModel.find()) {
...
}
The error is:
Type 'Query<IMyDocumentType[], IMyDocumentType>' must have a '[Symbol.asyncIterator]()' method that returns an async iterator.
Ignoring the error and running the code works fine.
Am I missing something or is there missing type information in mongoose 5.11.14 that #types/mongoose had?
mongoose 5.11.14
node.js 12.19.0
Yes, there is missing type information.
#types/mongoose#5.10.3 defines [Symbol.asyncIterator] on DocumentQuery, and Query extends DocumentQuery.
mongoose#5.11.14 does not define [Symbol.asyncIterator] on Query.
This method must be defined on any interface that is used on the right-hand side of a for await...of statement.
I do notice that both packages define a QueryCursor which extends stream.Readable. This class does define a [Symbol.asyncIterator] method, and the cursor() method of the Query interface returns an instance of QueryCursor. Try to see if the following compiles and runs as you expect without using #types/mongoose:
for await (const document of db.myModel.find().cursor()) {
// ^^^^^^^^^
...
}

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

Azure Search - Error

When trying to index documents we are getting this error:
{"Token PropertyName in state ArrayStart would result in an invalid JSON object. Path 'value[0]'."}
Our code for indexing using the .NET library is :
using (var indexClient = new SearchIndexClient(searchServiceName, indexName, new SearchCredentials(apiKey)))
{
indexClient.Documents.Index(IndexBatch.Create(IndexAction.Create(documents.Select(doc => IndexAction.Create(doc)))));
}
Does anyone know why this error occurs?
The issue is because of an extra call to IndexAction.Create. If you change your indexing code to this, it will work:
indexClient.Documents.Index(IndexBatch.Create(documents.Select(doc => IndexAction.Create(doc))));
The compiler didn't catch this because IndexBatch.Create has a params argument that can take any number of IndexAction<T> for any type T. In this case, T was a collection, which is not supported (documents must be objects, not collections).
The programming model for creating batches and actions is changing substantially in the 1.0.0-preview release of the SDK. It will be more type-safe so that mistakes like this are more likely to be caught at compile-time.

Resources