Having developed my own TypeScript ORM, I've implemented a unique approach where the model classes used for INSERT operations are distinct from the ones utilized for SELECT queries in order to maintain immutability...
- The INSERT models contain optional properties, such as fields that will be automatically filled by the database using default values or SQL TRIGGERs.
- In contrast, SELECT models do not include any optional properties; they always consist of a scalar value or
null
, neverundefined
.
For instance, if we have two SQL tables - user
and blog
- this results in separate models:
class Insert_user {
readonly id: string;
readonly username: string;
readonly joined_at?: string; // Optional due to default value set by SQL during INSERT
constructor(props: Insert_user) { Object.assign(this, props); Object.freeze(this); }
}
class Select_user {
readonly id: string;
readonly username: string;
readonly joined_at: string; // Guaranteed presence when fetching an existing record
constructor(props: Select_user) { Object.assign(this, props); Object.freeze(this); }
}
class Insert_blog {
readonly id: string;
readonly blog_title: string;
readonly view_count?: number; // Optional because of default value set by SQL during INSERT
constructor(props: Insert_blog) { Object.assign(this, props); Object.freeze(this); }
}
class Select_blog {
readonly id: string;
readonly blog_title: string;
readonly view_count: number; // Always present when retrieving an existing record
constructor(props: Select_blog) { Object.assign(this, props); Object.freeze(this); }
}
I aim to create multiple functions capable of accepting "Insert" models, with the typing system correctly inferring the corresponding "Select" model based on the input. For example:
type AnyInsertModel = Insert_user | Insert_blog;
type AnySelectModel = Select_user | Select_blog;
function takeAnInsertModelButReturnItsSelectModel(insertModel: AnyInsertModel) {
// Data from insert model is inserted into SQL database
// Once the INSERT operation is completed, data is selected accordingly
const selectModel = {/* Data retrieved through SELECT */} as Select_???;
}
/**
* The output variable below should have type Select_user
*/
const selectedUser = takeAnInsertModelButReturnItsSelectModel(new Insert_user({id: 'd110ec70-9a16-4ad0-a73b-82e241a054eb', username: 'Neo'}));
/**
* The output variable below should have type Select_blog
*/
const selectedBlog = takeAnInsertModelButReturnItsSelectModel(new Insert_blog({id: '2068bc9d-f19d-4043-a13a-6af4b2207be2', blog_title: 'I like milk'}));
I want the types to be determined simply from function arguments without necessitating redundant generics for each function call (though generics in function definitions are acceptable), given that the argument already carries the appropriate type information.
It seems like I might have found a solution myself (see answer below), but I'm open to exploring alternative approaches too.