I'm writing the implementation of an insert post use case. I'm studying the Clean Architecture and I want to create an blog API, where you can create, read, update and delete posts.
The Clean Architecture principles says you have to isolate your application into layers, where each layer is responsible to abstract your code as much as possible. I divided my code into three main layers: core (the most "immutable" abstractions, where I define the data models and the use cases), app (where I wrote the main application code, like validations and other stuff) and resources (the place for the less abstract code, like database connection, adapters, etc).
The general data was abstracted into the core layer, since it's the most fundamental information of my API. But the general data (most specifically id, created_at, updated_at, is_deleted fields) was abstracted into the app layer, because it's database dependent - the id field can be both a number (for general SQL databases) or a string (for MongoDB, for example). Then, i just join the two abstractions into one single model to create the "definitive" application models.
But there's a problem with this: the use cases abstractions uses the id data to define its implementation rules. By example, you have the InsertPost use case, where the return data is the id of the new created post. Since the id is only defined at the app layer, and Clean Architecture don't allow you to request data from more external layers, how can this abstraction know what type of id he's returning? The solution I found is to abstract the id type into the two possible types (number or string), and only provide the definitive type at the app layer.
This is the described InsertPost use case:
// ----- IMPORTS -----
type Id = number | string;
interface Post<IdType extends Id = Id> {
title: string
description: string
body: string
author_id: IdType
}
interface SuccessfulResponse<T> {
results: Array<T>
}
// -------------------
export namespace InsertPost {
export type Params<IdParam extends Id = Id> = Post<IdParam>
}
export interface InsertPost<IdType extends Id> {
insert: (params: InsertPost.Params) => Promise<SuccessfulResponse<IdType>>
}
I wrote the Params type from the InsertPost namespace to have a Generic Type that only accepts types derived from the Id type. The IdType should also have an default type when no type is given. My problem is that TypeScript is using the default Id type no matter if a valid alternative type was provided or not.
This is my implementation of the InsertPost use case:
// ----- IMPORTS -----
type Id = number | string;
interface Post<IdType extends Id = Id> {
title: string
description: string
body: string
author_id: IdType
}
interface SuccessfulResponse<T> {
results: Array<T>
}
namespace DefaultData {
export type Id = number
export type IsDeleted = boolean
export type CreatedAt = Date
export type UpdatedAt = Date
}
interface DefaultData {
id: DefaultData.Id
is_deleted: DefaultData.IsDeleted
created_at: DefaultData.CreatedAt
updated_at: DefaultData.UpdatedAt
}
interface InsertPostRepository {
insertPost: (user: Post) => Promise<DefaultData.Id>
}
function successfulResponseDataFormatter<T>(data: Array<T> | T): SuccessfulResponse<T> {
return {
results: Array.isArray(data) ? data : [data]
};
}
namespace InsertPost {
export type Params<IdParam extends Id = Id> = Post<IdParam>
}
interface InsertPost<IdType extends Id> {
insert: (params: InsertPost.Params) => Promise<SuccessfulResponse<IdType>>
}
// -------------------
export class InsertPostService implements InsertPost<DefaultData.Id> {
constructor(private readonly insertPostRepository: InsertPostRepository) {}
async insert(params: InsertPost.Params<DefaultData.Id>): Promise<SuccessfulResponse<DefaultData.Id>> { // TypeScript throws an error at this line
const { title, description, body, author_id } = params;
// Validation functions here
const id = await this.insertPostRepository.insertPost({
title,
description,
body,
author_id
});
return successfulResponseDataFormatter(id);
}
}
TypeScript throws the following errors:
Property 'insert' in type 'InsertPostService' is not assignable to the same property in base type 'InsertPost'.
Type '(params: Params) => Promise<SuccessfulResponse>' is not assignable to type '(params: Params) => Promise<SuccessfulResponse>'.
Types of parameters 'params' and 'params' are incompatible.
Type 'Params' is not assignable to type 'Params'.
Type 'Id' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
From what I've tested, this only happens when I export the type from inside a namespace (unfortunately, I need to use it). I really can't see where I'm going wrong; is there something I'm missing to code, or is this a TypeScript bug?