
I hung up a punching bag in my basement, stuck a stock photo of a typical manager on it and stuffed the speaker inside so that it would lose phrases that make me angry. For example, the pear says: “Business does not need your ideal code. He needs to solve the problem so that the profit covers the costs. If you need govnod for this, then there will be govnokod. ” And I start to bludgeon.
Recently, I added a note to the pear: “Types are difficult and unnecessary.” At this moment I hit so hard that my arm almost breaks. Because that's enough for me. A couple of months ago, I experienced one of the most egregious cases in my career.
')
My friend Anton asked me to help with the solution for one large-large corporation. I agreed, and we climbed into the bottomless abyss of corporate absurdity, crunch, wars with incomprehensible colleagues and all kinds of injustice. We can not say anything, so we will talk about types, so that such garbage would never be repeated.
one
- ( Anton rcanedu ) I was assigned to develop an internal platform tool - a library for expressing software entities in the form of object-oriented models and for working uniformly with the services API. That is, a tool for interacting with data that goes from the source to the display and back.
In this way a huge number of formatting, conversions, calculations and conversions. Huge structures, complex hierarchies, numerous connections with everything. In such a web is very easy to get lost. You see pieces of data and you do not know what can and cannot be done with them. Spending a lot of time to figure it out. This greatly delays the development. The problem is solved only by a good description of the types.
Due to the lack of appropriate typing in many solutions, it becomes more difficult to achieve the same program behavior at runtime and at compile time. Types must give full confidence that everything is the same and will also occur during execution. The same can give tests. It is best to rely on both. But if you choose between types and tests, types are much safer and cheaper.
When you do the front, there are two sources of problems - the user and the backend. With the user, everything is simple - there are many convenient libraries and frameworks abstracting from user I / O (react, angular, vue, and others).
In interaction with backend another story. Its types are numerous, and implementations - darkness. One standard approach to the description of the data is not defined. Because of this, they came up with crutches like “normalization of the data structure”, when all incoming data are reduced to a rigid structure, and if something goes wrong, exceptions start or work in abnormal mode. This should speed up and simplify development, but in fact a lot of documentation, UML diagrams, feature descriptions are required.
The architectural problem in the interaction of the client and server side arose because the frontend matured. It became a full-fledged business, and not just a layout on top of the backend. Previously, the interaction of the client and server was set only by the developers of the server side. Now the fronts and backs are forced to negotiate and cooperate closely. We need a tool that will allow us to structure work with the API of data source services, avoid losing the truth of data, and at the same time simplify further conversions. This problem should be solved by the whole community.
- (Phil) If you are a backender, you have plenty of adult solutions and data modeling practices for the application. For example, in C # you figure the data model class. You take a lib, any EntityFramework, lib delivers you the attributes with which you cover your models. You tell Libe how to reach the base. Then you use its interface for manipulating this data. This thing is called ORM.
In the frontend, we still have not decided how best to do it - we are looking for, trying, writing absurd articles, then refuting everything, starting anew and still will not come to a single solution.
- (Anton) Everything that I wrote earlier had one big problem - narrow specialization. Each time the library was developed from scratch and each time it was sharpened for one type of client-server interaction. All this is due to the lack of suitable typing.
I believe that without static typing it is difficult to imagine a universal library for working with API and domain expression. There will be a lot of reflection in it, it will contain a huge amount of documentation, it will be overgrown with various applications indicating practices for a particular type. This is not a simplification.
A good universal tool of this kind should give a complete picture of the data in any slice in order to always know exactly how to work with this data, and why they are needed.
- (Phil) Need lib, which will allow detailed description and management of each entity, obtaining data for this entity from different resources with different interfaces, from REST API and json-rpc to graphQL and NQL. Which will allow to keep the expanding code base and structure in strictness and order. Simple and intuitive to use. At any time providing a complete and accurate description of the state of entities. We want to abstract the modules of our users from the data layer as much as possible.
First we looked at the existing one. We did not like anything. For some reason, all the libraries for working with data are made either on js, or with a bunch of any outside. These any spoil everything. They turn a blind eye to the developers, saying that such a library will not help you much, that you will not be able to navigate the types of your data, you will not be able to express their connections. Everything gets worse when using multiple APIs of different types or it is heterogeneous.
All of these libraries were not sufficiently protected by types. Therefore, they create more problems than they solve. It's easier not to use them, but to make your domain-specific solutions.
Therefore, instead of the narrow-mindedness to which the task stood, we decided to do much more powerful and abstract - suitable for everything. And we incredibly believed that we were right, because it is only in this that truly good things are created.
2
- (Anton) As often happens, I waited for all types of access to be allowed into the company's repository. And this can take several weeks. In my case, it took only one. At this time, based on my experience in creating similar libraries, I decomposed the task, estimated the time frame.
The problem is that before, like everything, I made very narrowly specialized solutions. The work on the universal tool attracted additional problems. The type system was extremely complex, and neither I nor anyone else had any experience in designing this. Worse, the developers around me didn’t imagine static typing at all.
But I started doing what I thought was right. I told the Daily what I was doing and why, but basically nobody understood me. My questions to the team regarding the problem that had arisen always remained unanswered. It was as if I did not exist. A dude who does something very complex and incomprehensible.
I understood that javaScript here will not work in any way. I needed a YAP with a powerful typing model, excellent interaction with javaScript, which has a large community and a serious ecosystem.
- (Phil) I waited a long time for the day when Anton understood the beauty of typeScript.
- (Anton) But there are a number of problems in it that drive you crazy. There is a typification, but the user still doesn’t have a perfect match between the execution of the program and the execution of the user. At first, Typscript seems complicated. He has to endure. You always want to take and throw some object or any. Gradually you go deep into the language, into its type system, and then the interesting begins to happen. He becomes magical. You can type everything at all.
- (Phil) For the first time in our lives, we agreed on something and started.
The first step - the user must describe the data scheme. We pretend how it looks. Something like this:
type CustomerSchema = {
id: number;
name: string;
}
const Customer = new Model<CustomerSchema>(‘Customer’);
, , . id, , , .
, . , , -. , , .
, . — - , . , , . : , , . . :
/**
name: String
String - js -: StringConstructor
*/
const customerSchema = Schema.create({
id: Number,
name: String,
});
. , , . , Number String . : , . :
const customerSchema = Schema.create({
id: 1,
name: 2,
});
. `Schema.create` , . `if (!(property instanceof String)) throw new Error(« , »)`. .
-, , -, . , .
, , .
. , Schema.create.
:
//
type Map<T> = {
[key: string]: T | Map<T>;
};
/**
,
,
.
*/
type NumberType = Template<NumberConstructor, number, 'number'>;
type StringType = Template<StringConstructor, string, 'string'>;
type SymbolType = Template<SymbolConstructor, symbol, 'symbol'>;
type BooleanType = Template<BooleanConstructor, boolean, 'boolean'>;
type DateType = Template<DateConstructor, Date, 'date'>;
interface ArrayType extends Array<ExtractTypeValues<Types>> {};
type Types = {
Number: NumberType;
String: StringType;
Symbol: SymbolType;
Boolean: BooleanType;
Date: DateType;
Array: ArrayType;
};
//
type MapTypes= Map<ApplyTemplate<Types>>;
// -
type Declaration = ExtractInputTypes<MapTypes>;
interface Schema<...> {
// , .
create: <T extends Declaration>(
declaration: ConvertInstanceTypesToConstructorTypes<T>
) => Schema<T>;
};
, . , , , , .
,type CustomerSchema = {
id: number;
name: string;
};
const customerSchema: CustomerSchema = Schema.create({
id: Number,
name: String,
});
. , , .
. any. — any, 100%, , .
, , , . . `Schema.create` . 99% , . , . — ! , .
. , , , . . :
const customerSchema = Schema.create({
id: Number,
name: String,
});
// vscode : id, name
Schema.raw(customerSchema).
//
// .
Schema.raw(customerSchema).id;
// .
Schema.raw(customerSchema).neId;
. :
const customerSchema = Schema.create({
id: Number,
name: String,
});
if (true) {
customerSchema.add({gender: String});
}
// ,
// gender.
// ,
// .
Schema.raw(customerSchema).
, , , . gender, , (, , this !). - . , , .
, . , , . , . .
:
const customerSchema = Schema.create({
id: Number,
name: String,
});
// customerSchema.add({gender: String});
// , .
// :
const customerWithGenderSchema = customerSchema.add({gender: String});
// .
// :
Schema.raw(customerWithGenderSchema).
// id, name, gender
//
Schema.raw(customerSchema).
// id, name
, , . , .
:
const customerSchema = Schema.create({
id: Number,
name: String,
});
const repository = RepositoryManager.create(openApiDriver, {
// config
});
const Customer = Model.create(repository, customerSchema);
Customer.getAll().first().
// ide , id, name gender.
// ,
Customer.getAll().first().age;
// . , ,
// .
getAll .
:
type MapSchemaToDriver<S extends Schema, D extends Driver> =
InferSchemaDeclaration<S> extends SchemaDeclaration
? InferDriverMethods<D> extends DriverTemplate<IMRReader, IMRWriter>
? Repository<InferSchemaDeclaration<S>, InferDriverMethods<D>>
: never
: never;
interface Repository<D extends Driver, S extends Schema> {
get: <T extends DSLTerm>(dsl: ParseDSLTerm<T>) => MapSchemaToDriver<S, D> extends RepositoryTemplate ? ApplyDSLTerm<MapSchemaToDriver<S, D>, T> : Error<’type’, ‘Type error’>;
}
, :
«, B, A, , , , A. . - ».
. .
«» , , . , .
. , . :
« , *--.*, . , id . - , , ».
.
2.5
, , , . .
, — , , , . . , , .
, . , , , , . , , .
ODM ORM — IMR (Isomorphic Model Representation)
, , API . , . , select-where , .
— , , .
. . . , , , . , , , , .
, — , — , - .
.
3
— () , , . , , , . . , , , .
- , , « ». , . , — . , , ,
, .
. , , . , , , . . . , . , , , , .
— () , , . . , , , .
, , . . , .
— () , . , , . . , . - , , , , .
, - . , . , .
, , . , . .
4
— () , — . ! ?! , , , , , ODM/ORM - , . , . , , «- , ».
, , . . , , . .
, , - , . — , .
, . -:
/*
.
— .
.
*/
import { View } from '@view';
import { Logger } from '@utils';
// — , .
// .
import { Robot } from '@domain/models';
// ,
//
function foo() {
Robot.fetch({
location: {
area: 2,
free: true,
key: 'f49a6712', // - compile-time checked
}
})
.then(View.Grid.display)
.catch(Logger.error);
}
, . , — js/ts . , . - - , , , — Result .
. - Logger.Error, .
/*
:
,
- .
-
.
:
*/
import { Schema, Model } from 'imr';
import { Logger } from '@utils';
import { View } from '@view';
import { robotSchema } from '@domain/schemas';
import { batteryStationService } from '@domain/infrastructure/services/batteryStation';
import { Robot } from '@domain/models';
import { batteryNode } from '../services/nodeNames';
//
// , , ,
// , «»
const robotSchemaWithBattery =
robotSchema
.add('battery', Schema.union('li-ion', 'silicon'))
.remove('speed');
// ,
// :
function foo(nodeName) {
// -: -,
if (nodeName === batteryNode) {
// ,
const CustomerRobot = Model.create(robotSchemaWithBattery, batteryStationService);
CustomerRobot
// .
// , , 'li-notIon'
.fetch({ filter: { battery: 'li-ion' } })
// .
// , , , .
// , ,
// , .
.then(View.Grid.display)
.catch(Logger.error)
} else {
Robot
.fetch()
.then(View.Grid.display)
.catch(Logger.error)
}
}
5
— () , , , - , . , - , , .
.
, , — . . , — , , . . , , — , .
, : . . .
— . , .
: rcanedu, arttom