The story of how to spend two days on rewriting the same code many times.
In this article I will omit the details about Hapi, Joi, routing and validate: { payload: ... }
, implying that you already understand what it is about, like the terminology, a la "interfaces", "types" and the like . I’ll tell you only about the step-by-step, not the most successful strategy of my own learning these things.
Now I am the only backend developer (writing code) on the project. Functionality is not the essence, but the key essence is a rather long profile with personal data. The speed of work and the quality of the code are tied to my little experience of independent work on projects from scratch, even less experience with JS (only 4th month) and, incidentally, very crookedly, I write in TypeScript (hereinafter - TS). The deadlines are compressed, the rolls are compressed, edits are constantly arriving and it turns out to first write the code of business logic, and then the interfaces from above. Nevertheless, technical debt is able to catch up and knock on the cap, which approximately happened to us.
After 3 months of working on the project, I finally agreed with my colleagues on the transition to a single vocabulary, so that the properties of the object were called and written everywhere in the same way. Under this business, of course, undertook to write the interface and tightly stuck with him for two working days.
A simple user profile will be used as an abstract example.
Assume that tests have already been written to this code, it remains to describe the data:
interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' };
Well, everything is clear and very simple. All this code, as we remember, is on the backend, or rather, in api, that is, the user is created based on the data that came through the network. So we need to validate the incoming data and help Joi with it:
const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
The solution "in the forehead" is ready. The obvious disadvantage of this approach is that the validator is completely detached from the interface. If during the life of the application the fields are changed / added or their type is changed, then this change will need to be manually tracked and indicated in the validator. I think these responsible developers will not be until something falls. In addition, in our project, the questionnaire consists of 50+ fields on three levels of nesting and it is extremely difficult to understand this, even knowing everything by heart.
We cannot simply specify const joiUserValidator: IUser
, because Joi
uses its own data types, which Type 'NumberSchema' is not assignable to type 'number'
in compiling an error of the type Type 'NumberSchema' is not assignable to type 'number'
. But there must be a way to validate the interface?
Probably, I incorrectly googled, or poorly studied the answers, but all the decisions came down to either extractTypes
and some fierce bicycles, such as this :
type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : /* ... more schemata ... */ never;
Why not. When I asked people for their task, I received in one of the answers, and later, and here, in the comments (thanks to keenondrums ), links to these libraries:
https://github.com/typestack/class-validator
https://github.com/typestack/class-transformer
However, there was an interest to find out for myself, to understand better the work of TS, and nothing was pressing to solve the problem momentarily.
Since I had no previous affairs with statics, the above code discovered America in terms of using ternary operators in types. Fortunately, it was not possible to apply it in the project. But I found another interesting bike:
interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
TypeScript
with rather tricky and mysterious conditions allows you to get, for example, keys from the interface, as if this is a normal JS object, but only in the type
construction and through the key in keyof T
and only through generics. As a result of the work of the UserKeys
type, all objects implementing interfaces should have the same set of properties, but the types of values can be arbitrary. This includes hints in the IDE, but still does not allow unambiguously to identify the types of values.
There is another interesting case that could not be used. Perhaps you can tell why this is needed (although I partially guess that the application example is missing):
interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
You can explicitly specify types, and using "OR" and extracting properties, you can get locally workable code:
type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
The problem of this code manifests itself when we want to pick up a valid object, for example, from the database, that is, TS does not know in advance what type of data it will be - simple or Joi. This can cause an error when trying to perform math operations on a field that is expected to be a number
:
const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10; // error TS2362: The left-hand side of an arithmetic operation must be of type'any', 'number', 'bigint' or an enum type
This error comes from Joi.NumberSchema
because age can be more than just a number
. For that fought for it and ran.
Somewhere at this point, the working day came to a logical conclusion. I took a breath, drank coffee and erased this pornography to hell. It is necessary to read these your internet less! The time has come take a shotgun and Brainstorm:
type
construct is clearly capable of something else.We write generic interface with default types:
interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; }
For Joi, you could create a second interface, inheriting the main one in this way:
interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {}
Not good enough, because the next developer can expand IUserJoi
with a light heart or worse. A more limited option to get similar behavior:
type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>;
We try:
const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
UPD:
To wrap in Joi.object
I had to contend with the TS2345
error and the simplest solution was as any
. I think this is not a critical assumption, because the object is higher all the same on the interface.
const joiUserInfo = { info: Joi.object(joiUser as any).required() };
Compiles, at the place of use it looks neat and in the absence of special conditions always sets the types by default! Beauty…
... what I spent two working days
What conclusions can be drawn from all this:
What we won:
type
abstraction, visually unloading the code from monstrous constructions.Moral: Experience is priceless, for the rest there is a map of the "World".
You can see, touch, run the final result:
https://repl.it/@Melodyn/Joi-by-interface
Source: https://habr.com/ru/post/450238/
All Articles