Picture a Typescript library that serves as a database interface, giving developers the ability to specify record attributes/columns/keys to be retrieved from the database. Is it feasible to return a type that includes the keys specified by the developer?
Let's consider a record definition like this...
type GenericRecord = { [key: string]: string | number };
type UserRecord = {
id: string;
name: string;
age: number;
};
...the user should be able to load a record in the following manner. The desired return type should be {name: string, age: number}
or something similarly detailed.
const userLoader = new RecordLoader<UserRecord>();
const user = userLoader.add("name").add("age").load();
Here is an attempt at achieving this:
class RecordLoader<FullRecord extends GenericRecord, PartialRecord extends Partial<FullRecord> = {}> {
private attrsToLoad: (keyof FullRecord)[] = [];
constructor(attrsToLoad: (keyof FullRecord)[] = []) {
this.attrsToLoad = attrsToLoad;
}
add(attrName: keyof FullRecord) {
this.attrsToLoad.push(attrName);
// Can we use `attrName` to dynamically define key names and value types for `PartialRecord` here?
return new RecordLoader<FullRecord, PartialRecord & { [attrName: string]: string }>();
}
load() {
return loadData<PartialRecord>(this.attrsToLoad);
}
}
function loadData<PartialRecord extends GenericRecord>(keys: (keyof PartialRecord)[]) {
// Load data and populate return object here.
return {} as PartialRecord;
}
While working on the add
method, I'm struggling to create the intersected type that accurately specifies the key passed into the method. Is there a way for the code and type system to interact in this manner?
I could make the add
method generic and pass both the key and type into it, but I'd prefer not to repeat the key name in the generic declaration and the method itself, along with defining the value type in the record and the generic declaration.
add<T extends Partial<FullRecord>>(attrName: keyof FullRecord){
this.attrsToLoad.push(attrName);
return new RecordLoader<FullRecord, PartialRecord & T>();
}
const userLoader = new RecordLoader<UserRecord>();
const user = userLoader.add<{name: string}>("name").add<{age: number}>("age").load();
// Type of user is `{name: string} & {age: number}`