Augment an interface to remove the indexer - typescript-typings

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

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.

Reference imported object based on env

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];

How to customise firebase DocumentData interface?

I'm using firebase#5.5.8 and typescript#3.1.4
whenever I create a document from firestore i get an object of type firebase.firestore.DocumentReference as expected
If I call the get(options?: firebase.firestore.GetOptions|undefined) I'll get a object of type firebase.firestore.DocumentSnapshot as expected
If i call data(options?: firebase.firestore.DataOptions|undefined) I'll get either a firebase.firestore.DocumentData object or undefined, as expected
Now, on my manager object, I know what I'm writing to the databse, so I can make the assertion that whatever DocumentData you get out of my manager, you'll get a Client as shown
export interface Client {
name: string;
website?: string;
description?: string;
visible: boolean;
}
I want to create an interface for my manager object that expresses that. So I've tried:
export interface FirebaseDocumentSnapshot<T> extends $firebase.firestore.DocumentSnapshot {
data(options?: $firebase.firestore.SnapshotOptions | undefined): T|undefined
}
export interface FirebaseDocumentReference<T> extends $firebase.firestore.DocumentReference {
get(options?: $firebase.firestore.GetOptions | undefined): Promise<FirebaseDocumentSnapshot<T>>
}
The problem i've got is here:
const client: Client = mapClient(args);
const result: FirebaseDocumentReference<Client> = await $db.add(client);
the error is:
[ts]
Type 'DocumentReference' is not assignable to type 'FirebaseDocumentReference<Client>'.
Types of property 'get' are incompatible.
Type '(options?: GetOptions | undefined) => Promise<DocumentSnapshot>' is not assignable to type '(options?: GetOptions | undefined) => Promise<FirebaseDocumentSnapshot<Client>>'.
Type 'Promise<DocumentSnapshot>' is not assignable to type 'Promise<FirebaseDocumentSnapshot<Client>>'.
Type 'DocumentSnapshot' is not assignable to type 'FirebaseDocumentSnapshot<Client>'.
Types of property 'data' are incompatible.
Type '(options?: SnapshotOptions | undefined) => DocumentData | undefined' is not assignable to type '(options?: SnapshotOptions | undefined) => Client | undefined'.
Type 'DocumentData | undefined' is not assignable to type 'Client | undefined'.
Type 'DocumentData' is not assignable to type 'Client'.
Property 'name' is missing in type 'DocumentData'. [2322]
const result: FirebaseDocumentReference<Client>
How can i declare the interfaces so I can know the type of the resulting object?
A simple solution would be to cast the return data:
const client = snapshot.data() as Client
#gal-bracha's solution will "work" in the sense that the TypeScript compiler will assume client is of type Client, but it doesn't prevent a runtime error if bad data ended up in Firestore. A safer solution when dealing with any data external to your app is to use something like Yup to validate data explicitly:
import * as yup from "yup";
const clientSchema = yup.object({
name: yup.string(),
website: yup.string().url().notRequired(),
description: yup.string().notRequired(),
visible: yup.boolean()
});
// You can use this elsewhere in your app
export type Client = yup.InferType<typeof clientSchema>;
// Usage
const client = await clientSchema.validate(snapshot.data());
This is more defensive as clientSchema.validate will throw an error if, for some reason, bad data ended up in Firestore. After validate(), you're guaranteed, that client is a Client and not just telling the TypeScript compiler "treat this as a Client even though at runtime, it may not be".
export declare function getDocs<T>(query: Query<T>): Promise<QuerySnapshot<T>>;

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];

Typescript declaration merging: override?

Is there a way to "override" when doing declartion merge, eg:
app.ts (express + nodejs):
import * as express from 'express';
var app = express();
app.use(function(req, res, next) {
console.log(req.headers.accept);
});
This fails with error:
error TS2339: Property 'accept' does not exist on type '{ [key: string]: string; }'.
Because in express.d.ts headers are declared like this:
headers: { [key: string]: string; };
So what i've tried to do, is create a definition of 'accept' :
declare module Express {
export interface Headers {
accept: String
}
export interface Request {
headers: Headers
}
}
But that does not work also (i can only add new members, not override them, right?):
error TS2430: Interface 'e.Request' incorrectly extends interface 'Express.Request'.
Types of property 'headers' are incompatible.
Type '{ [key: string]: string; }' is not assignable to type 'Headers'.
Property 'accept' is missing in type '{ [key: string]: string; }'.
So the only way to overcome this is change notation to:
req.headers.accept -> req.headers['accept']
Or i somehow can "redeclare" the headers property?
In Express, you can use the accepts method (accepts documentation):
if (req.accepts('json')) { //...
Or if you want them all, they are available as:
console.log(req.accepted);
To get other headers, use the get method (get method documentation)
req.get('Content-Type');
These are all included the the type definition for Express on Definitely Typed.

Resources