Jest test Typescript type as object - javascript

Say I have the following type:
export type Events = {
LOGIN: undefined
NAVIGATION: {
screen: string
}
SUPPORT: {
communication_method: 'chat' | 'email' | 'phone'
}
}
then I would like to make a test that ensures I do not write a type "key" more than 45 characters.
it('is compliant to event limitations', () => {
Object.keys(Events).forEach((key) => {
expect(key.length).toBeLessThan(45)
})
of course TS complains 'Events' only refers to a type, but is being used as a value here. but is there an elegant way to 'convert' it to an value?

The Types in TypeScript are used only in compile time and removed during the runtime.
When you run your program there will be no type Events in your source.
Your code:
export type Events = {
LOGIN: undefined
NAVIGATION: {
screen: string
}
SUPPORT: {
communication_method: 'chat' | 'email' | 'phone'
}
}
will compile to JS to nothing (type will be removed).
If you have a variable that uses this type you can check that
export type Events = {
LOGIN: undefined
NAVIGATION: {
screen: string
}
SUPPORT: {
communication_method: 'chat' | 'email' | 'phone'
}
}
const myVar: Events = {...}
Then you can check that variable:
it('is compliant to event limitations', () => {
Object.keys(myVar).forEach((key) => {
expect(key.length).toBeLessThan(45)
})
Another way it to use custom transformer library like ts-transformer-keys
I'll not recommend doing, though, but here is it for completeness.
import { keys } from 'ts-transformer-keys';
export type Events = {
LOGIN: undefined
NAVIGATION: {
screen: string
}
SUPPORT: {
communication_method: 'chat' | 'email' | 'phone'
}
}
const eventKeys = keys<Events>();
console.log(eventKeys); // ['id', 'name', 'age']
it('is compliant to event limitations', () => {
Object.keys(eventKeys).forEach((key) => {
expect(key.length).toBeLessThan(45)
})

Related

How to get "to_do" from a notion block using typescript?

Recently I tried final version of Notion API. Using typescript I made a retrieve block children request:
(async () => {
const blockId = process.env.BLOCK_ID;
const response = await notion.blocks.children.list({
block_id: blockId,
page_size: 50,
});
console.log(response.results[1].to_do);
})();
Error message
For some reason typescript tells me that to_do doesn't exist in type (PartialBlockObjectResponse | BlockObjectResponse). I looked at type definition and... it was there:
declare type BlockObjectResponse = {
// ...
{
type: "to_do";
to_do: {
rich_text: Array<RichTextItemResponse>;
color: ApiColor;
checked: boolean;
};
object: "block";
id: string;
created_time: string;
created_by: {
id: IdRequest;
object: "user";
};
last_edited_time: string;
last_edited_by: {
id: IdRequest;
object: "user";
};
has_children: boolean;
archived: boolean;
}
// ...
}
I tried making type guard
function isToDo(value: PartialBlockObjectResponse | BlockObjectResponse): value is BlockObjectResponse {
return "to_do" in value;
} /* Error: TS2304: Cannot find name 'PartialBlockObjectResponse'. */
and importing type from package
import type {PartialBlockObjectResponse} from "#notionhq/client/build/src/api-endpoints";
// Error: Module '"#notionhq/client/build/src/api-endpoints"' declares 'PartialBlockObjectResponse' locally, but it is not exported.
Neither helped.
(Frustratingly) That package does not export its types. There's an open issue about it in the repo.
But you can work around this using a generic with your predicate function:
TS Playground
import {Client} from '#notionhq/client';
declare const notion: Client;
declare const blockId: string;
function isTodo <T extends Record<string, unknown>>(obj: T): obj is T & { type: 'to_do' } {
return 'type' in obj && obj.type === 'to_do';
}
(async () => {
const response = await notion.blocks.children.list({block_id: blockId, page_size: 50});
for (const result of response.results) {
if (isTodo(result)) {
result.to_do; /*
^^^^^
is now this type:
{
rich_text: RichTextItemResponse[];
color: ApiColor;
checked: boolean;
}
*/
}
}
})()
My answer assumes that you cloned this notion-sdk-typescript-starter repo.
The way I solved this issue was to go to the api-endpoints.d.ts file and replace PartialBlockObjectResponse (inside the ListBlockChildrenResponse type) with a new type that matches the desired response structure you are looking for. Here is what that looks like:
export declare type ListBlockChildrenResponse = {
type: "block";
block: EmptyObject;
object: "list";
next_cursor: string | null;
has_more: boolean;
results: Array<DesiredBlockObjectResponse | BlockObjectResponse>;
// ^^^ here is where PartialBlockObjectResponse is deleted
// and replaced
The new type that I implemented looks like this:
export declare type DesiredBlockObjectResponse = {
type: "block";
block: {
type:string,
object: string,
id: string,
parent: { type: string, page_id: string },
// ... more info if desired
}
object: "block"
};
I hope this helps!

Is it possible to get the file path of the function being executed in React/JavaScript?

I am using create-react-app to build my application and new relic's noticeError() to log errors.
Below, you can see an example of how I log errors:
var err = new Error('Report caught error to New Relic');
// exported from logger.ts
logError(err, {
hint:
'Check the validateToken() function. The JWT token might have expired',
filePath: 'src/routes/Routes.tsx',
});
logger.ts
import { getLocalStorage } from '../utils/localStorageHandler';
import LOCALSTORAGE_KEYS from '../constants/localStorageKeys';
import { CandidatePersonalDetail } from '../graphql/declarations';
interface Attributes {
errorType: string;
mobile?: string;
userID?: number;
role?: string;
name?: string;
}
// Define and export your error types from here
export const ERROR_TYPES = {
API_ERROR: 'API_ERROR',
};
export const logError = (
err: any,
customAttributes?: { [key: string]: string | number },
callback?: () => void,
): void => {
const attributes: Attributes = {
errorType: ERROR_TYPES.API_ERROR,
...customAttributes,
};
const personalDetails:
| CandidatePersonalDetail['candidate']
| null = getLocalStorage(LOCALSTORAGE_KEYS.USER_PERSONAL_DETAILS);
// If personal details exist in local storage
if (personalDetails && Object.keys(personalDetails).length) {
if (personalDetails.mobile) {
attributes.mobile = personalDetails.mobile;
}
if ((personalDetails as any).role) {
attributes.userID = personalDetails.id;
attributes.name = `${personalDetails.firstName} ${personalDetails.lastName}`;
attributes.role = (personalDetails as any).role;
}
}
(window as any).newrelic.noticeError(err, attributes);
if (callback) {
callback();
}
};
I am manually adding the file path to help the developers as much as I can to debug. Is it possible to automate this by accessing the file path whenever logError() function is called so that I can pass it as a custom attribute to new relic? I am ok with absolute or relative paths. Thanks in anticipation.

Using typescript generics properly

I have an interface of the following format which describes database methods like so:
export default interface IRepository {
createAndSave(data: ICreateUserDTO): Promise<User>
findById<T>({ id }: { id: number }): Promise<T | null> // right here
...
}
As you can see from the snippet above, findById method is meant to take in a type and return a resolved promise of type T or a null value. I go-ahead to implement this in a class like so.
class DatabaseOps {
private ormManager: Repository<User>
...
async findById<User>({ id }: { id: number }): Promise<User | null> {
const t = await this.ormManager.findOne({
where: { id },
})
return t
}
...
}
When I try to create the findById method like that, typescript gives this error of this format
Type 'import("../src/user/infra/typeorm/entities/User").default' is not assignable to type 'User'.
'User' could be instantiated with an arbitrary type which could be unrelated to 'import("../src/audience/infra/typeorm/entities/User").default'
I tried to use typescript assertion to override this error like so
class DatabaseOps {
private ormManager: Repository<User>
...
async findById<User>({ id }: { id: number }): Promise<User | null> {
const t = await this.ormManager.findOne({
where: { id },
})
return t as Promise<User> // here
}
...
}
but I still get the error, I am not really sure what to do from this point onward.
Here is what the User model definition looks like, I am making use of TypeORM
export default class User {
#PrimaryGeneratedColumn('uuid')
id: string
#Column({
type: 'json',
nullable: true,
})
data: object
#Column({ type: 'tinyint', default: 1 })
status: number
...
}
What could be the cause of this and how do I rectify it? Any help will be appreciated. Thank you very much!
The IRepository.findById method's type signature doesn't mean what you think it means.
When you write findById<T>, it means that the method promises to work with any type T. Whoever calls the method chooses which type it is. Kind of like this:
const r : IRepository = ...
const x = r.findById<User>( ... )
const y = r.findById<number>( ... )
consy z = r.findById<string>( ... )
... and so on
And since the caller of the method can choose any type, it means that the implementer of the method must implement it such that it can work with any type. So it can't be just User. It has to be any type, whatever the caller happens to choose.
Now, what you probably meant to do was to create not just a repository, but a repository of a certain thing. To do this, the generic parameter should be on the interface, not on the method:
export default interface IRepository<T, DTO> {
createAndSave(data: DTO): Promise<T>
findById({ id }: { id: number }): Promise<T | null>
...
}
Then you can implement IRepository<User, ICreateUserDTO> in your class:
class UserRepository {
...
async createAndSave(data: ICreateUserDTO): Promise<User> {
...
}
async findById({ id }: { id: number }): Promise<User | null> {
const t = await this.ormManager.findOne({
where: { id },
})
return t as Promise<User> // here
}
...
}

Pass in params to function by object spreading a params object not working

I want to pass in all the params to this function by spreading a params object like this SO answer:
public showToast(
content: string,
buttonLabel = 'Ok',
hideDelay?: number,
buttonAction?: Function,
uniqueKey?: string,
canBeCleared?: boolean,
templateCtrl?: Object
): Toast {
const toast = new Toast(
content,
this,
buttonLabel,
hideDelay,
buttonAction,
uniqueKey,
canBeCleared,
templateCtrl
);
toast.id = !this.lastToast ? 1 : this.lastToast.id + 1;
this.toasts.push(toast);
toast.top = toast.fromTop;
// hiding is handled in the ToastComponent
return toast;
}
My attempt:
const toastArgs = {
content: 'test toast',
buttonLabel: 'test button',
hideDelay: 1000,
buttonAction: () => 5,
uniqueKey: 'uk',
canBeCleared: true,
templateCtrl: { strl: 3 }
};
service.showToast(...Object.values(toastArgs));
The compile time error:
Expected 1-7 arguments, but got 0 or more.ts(2556)
toast.service.ts(18, 9): An argument for 'content' was not provided.
Why do I get this error? I have followed the SO answer.
You're getting this since Object.values(toastArgs) gets inferred as:
(true | "test toast" | "test button" | 1000 | (() => number) | "uk" | {
readonly strl: 3;
})[]
Typescript does not really have the concept of an array's 'length'. As far as typescript is concerned, toastArgs could be { x: true }, and it would satisfy the type of Object.values(toastArgs). You can test this out like so:
const toastArgsValid = {
content: 'test toast',
buttonLabel: 'test button',
hideDelay: 1000,
buttonAction: () => 5,
uniqueKey: 'uk',
canBeCleared: true,
templateCtrl: {strl: 3},
};
const toastArgsValuesValid = Object.values(toastArgsValid);
type ToastParamsValid = typeof toastArgsValuesValid;
const toastArgsInvalid = {
x: true,
};
const toastArgsValuesInvalid: ToastParamsValid = Object.values(
toastArgsInvalid,
); // No error here since the invalid type is a subtype of the valid type
One way to achieve what you're looking for is to refactor your showToast method to instead take a single object, instead of positional args:
public showToast({
content,
buttonLabel,
hideDelay,
buttonAction,
uniqueKey,
canBeCleared,
templateCtrl,
}: {
content: string;
buttonLabel: string;
hideDelay?: number;
buttonAction?: Function;
uniqueKey?: string;
canBeCleared?: boolean;
templateCtrl?: Object;
}): Toast {
// function body
}
You can then call:
service.showToast(toastArgs)
Alternatively you can also use a type assertion, without refactoring your code like so:
service.showToast(...(Object.values(toastArgs) as Parameters<typeof service.showToast>));
Object.values() is supported from es2017, ECMAScript_2017. And the default target version of TypeScript is ES3. The error can be fixed by just adding the compiler option --target es2017.

Typescript strongly typed key value pair declaration

I would like to declare a TypeScript interface for such json structure:
{
404: function() { alert( "page not found" ); },
400 : function() {...}
}
the key is number, and the value is function, do you know how to declare an interface in TypeScript for such data constraints?
Indexer
You can use numbers as keys in JavaScript if you use [] key access...
Let's start with your desired code...
var x = {
404: function() { alert( "page not found" ); },
400 : function() { alert("...");}
};
x.404();
The last statement above (the call to the 404 function) will error with Missing ; before statement, so you have to use...
x[404]();
While this will still get you type inference in TypeScript (var a = x[404]; - a will be type () => void) - it won't give you good auto-completion.
Interface for this:
interface HttpCodeAlerts {
[index: number]: () => void;
}
With Auto-Completion
Normally in JavaScript and TypeScript it is recommended that you use safer names. Simplistically, you need to start them with a letter:
var x = {
E_404: function() { alert( "page not found" ); },
E_400 : function() { alert("...");}
};
x.E_404();
Interface for this:
interface HttpCodeAlerts {
E_400: () => void;
E_404: () => void;
}
Framework Style
In most languages, the error is used more like this...
class HttpCode {
static OK = { responseCode: 200, reasonPhrase: 'Okay' };
static NotFound = { responseCode: 404, reasonPhrase: 'Not Found' };
};
alert(HttpCode.NotFound.reasonPhrase);
See TypeScript Objects as Dictionary types as in C#
var map: { [code: number]: ()=>void; } = { };
//usage
map[404] = ()=> { alert("page not found"); };
map[400] = ()=> { alert("..."); };
map["some"] = "some"; //compile error
//all in once
var map: { [code: number]: ()=>void; } = {
404: () => { alert( "page not found" ); },
400: () => { alert( "..." )}
};
This can probably be one of the answer :-
export interface clientSideFunction{
[code: number]: ()=>void;
}
use this interface by importing it using:-
import {clientSideFunction} from 'filePath';
It is not valid JSON structure thus not valid JavaScript (neither TypeScript).
Object keys should be strings. According to this answer numbers are always converted to strings.
Therefore I suggest to use explicit strings as keys in your JSON. Then you can model it in TypeScript like this:
interface ICodes {
"404": () => void;
[code: string]: () => void; // defines any string key to be function
}
var codes: ICodes = {
"404": function () { alert("page not found"); },
"400": function () {}
};
// call the function for code 404
codes["404"]();

Categories

Resources