Creating a custom Typescript type by leveraging Javascript variables as the key identifiers

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}`

Answer №1

If you want to achieve more accurate and predictable results, it might be beneficial to distribute N when it can act as a union:

  add<N extends keyof FullRecord>(attrName: N) {
    this.attrsToLoad.push(attrName);

    // distributing N over the Pick for better type predictability when N is a union
    return new RecordLoader<FullRecord, PartialRecord & (N extends N ? Pick<FullRecord, N> : never)>();
  }

Dealing with the load function can be tricky. Turning on exactOptionalPropertyTypes should make the original code function correctly without any issues.

  load() {
    // enabling exactOptionalPropertyTypes - loadData<PartialRecord> would work
    return loadData<{
      [K in keyof PartialRecord]-?: Exclude<PartialRecord[K], undefined>;
    }>(this.attrsToLoad);
  }

Playground


To prevent the addition of duplicate properties, exclude the keys already present in PartialRecord:

  add<N extends Exclude<keyof FullRecord, keyof PartialRecord>>(attrName: N) {

Playground

Answer №2

Shoutout to @kelsny for coming up with the answer!

  include<T extends keyof CompleteDataSet>(item: T) {
    this.itemsToInclude.push(item);
    return new DataSetLoader<CompleteDataSet, PartialDataSet & Pick<CompleteDataSet, T>>();
  }

Answer №3

A different method that streamlines the process of adding attributes only if they are not already included, and also allows for attribute removal. This new approach is immutable and not restricted to a specific key type:

Maybe there's room for improvement in terms of clarity, but this represents the best solution I could come up with.

type UserInformation = {
  id: string;
  name: string;
  age: number;
};

function fetchUserData<Input, Output>(attributes: (keyof Input)[]): Output {
    console.log(attributes);
    return {} as Output;
}

class DataRetriever<DataType, ExcludeData = DataType, OutputType = {}> {

    constructor(private attributes: (keyof DataType)[] = []) {
    }


    append<Key extends keyof (ExcludeData | DataType)>(attribute: Key) {
        if (this.attributes.includes(attribute)) {
            throw new Error('Attribute already exists.');
        }
        const updatedAttributes = this.attributes.slice();
        updatedAttributes.push(attribute);
        return new DataRetriever<DataType, Omit<ExcludeData, Key>, OutputType & Pick<DataType, Key>>(updatedAttributes);
    }

    eliminate<Key extends keyof (OutputType | DataType)>(attribute: Key) {
        const index = this.attributes.indexOf(attribute);
        if (index === -1) {
            throw new Error('Attribute does not exist.');
        }
        const updatedAttributes = this.attributes.slice();
        updatedAttributes.splice(index);
        return new DataRetriever<DataType, Pick<OutputType, Key> & ExcludeData, Omit<OutputType, Key>>(updatedAttributes);
    }

    retrieve(): OutputType {
        return fetchUserData<DataType, OutputType>(this.attributes);
    }
}

const userDataRetriever = new RecordLoader<UserInformation>();
const person = userDataRetriever
    .add('age')
    .add('id')
    .add('name')
    .load(); // ["age", "id", "name"]

const person2 = userDataRetriever
    .append('age')
    .append('age') // compile time error
    .retrieve();

const person3 = userDataRetriever
    .append('age')
    .eliminate('age')
    .append('age')
    .retrieve(); // ['age']

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for eliminating unicode characters from Graphql error messages

In my resolver, I have implemented a try and catch block where the catch section is as follows: catch (err: any) { LOG.error("Failed to get location with ID: " + args.id); LOG.error(err); throw new Error(err); ...

Developing maintenance logic in Angular to control subsequent API requests

In our Angular 9 application, we have various components, some of which have parent-child relationships while others are independent. We begin by making an initial API call that returns a true or false flag value. Depending on this value, we decide whether ...

Implementing Bootstrap 5 JS within an Angular 11 component TypeScript

I am currently working on a project that utilizes Angular 11 and we are aiming to integrate Bootstrap 5 native JS without relying on third-party libraries like ng-bootstrap, MDB, or ngx-bootstrap (jQuery is not being used as well). I understand that using ...

"Experience the power of utilizing TypeScript with the seamless compatibility provided by the

I'm attempting to utilize jsymal.safeDump(somedata). So far, I've executed npm install --save-dev @types/js-yaml I've also configured my tsconfig file as: { "compilerOptions": { "types": [ "cypress" ...

TypeScript: Defining a custom type based on values within a nested object

I'm attempting to generate a unique type from the value of a nested object, but encountering failure if the key is not present on any level of nesting. Can someone point out where I might be making a mistake? const events = [ { name: 'foo&apos ...

Exploring unit tests: Customizing an NGRX selector generated by entityAdapter.getSelectors()

Let's imagine a scenario where our application includes a books page. We are utilizing the following technologies: Angular, NGRX, jest. To provide some context, here are a few lines of code: The interfaces for the state of the books page: export int ...

`The error "mockResolvedValue is not recognized as a function when using partial mocks in Jest with Typescript

Currently, I am attempting to partially mock a module and customize the return value for the mocked method in specific tests. An error is being thrown by Jest: The error message states: "mockedEDSM.getSystemValue.mockResolvedValue is not a function TypeEr ...

In tsconfig.json, the compiler is not utilizing other tsconfig.json files when using the "extends"

I'm attempting to streamline my project by breaking up my tsconfig.json into separate files. I have one for the source files and another for the tests. However, when I utilize the extends field, it seems that only the base tsconfig.json is being utili ...

Is there a way to include the present date and time within a mat-form-field component in Angular?

I'm working on a mat-form-field to display the current time inside an input field. I've managed to insert it, but I'm struggling with the styling. Here's the snippet of code I'm using: <mat-label>Filing Time:</mat-label> ...

My reselect function seems to be malfunctioning - I'm not receiving any output. Can anyone help me

I'm looking to implement reselect in my code to retrieve the shopping cart based on product ids. Here's my reselect.ts file: import { createSelector } from "reselect"; import { RootState } from "../store"; export const shopp ...

Defining a property of an object within a Vue Class

If I were to write it in JavaScript version: export default { data() { return { users: {} } } } However, when using a class style component with vue-property-decorator: @Component export default class Login extends Vue { public title ...

Creating a new endpoint within the Angular2 framework using typescript

I am brand new to Angular2 and I would like to streamline my API endpoints by creating a single class that can be injected into all of my services. What is the most optimal approach for achieving this in Angular2? Should I define an @Injectable class sim ...

Unlocking the ability to retrieve data generated by the context within getServerSideProps beyond its boundaries (for transitioning from Create React App to Next.js)

I am currently utilizing Create React App for my react application but I am in the process of migrating to Next.js. Accessing URL data such as host, protocol, and query parameters has posed a significant challenge. After some trial and error, I realized t ...

Utilizing a powerful combination of Angular 5, PrimeNG charts, Spring Boot, and JHipster

I am facing an issue with creating charts using PrimeNG. The main challenge I'm encountering is the conversion of data from a REST API in Angular 5 (TypeScript) and retrieving the list of measurements from the API. I have an endpoint that returns my m ...

Is there a way to determine the height of the ion-footer?

Is there a way to obtain the height of the ion-footer element in Ionic2? I want to calculate the initial window height minus the ion-footer height, but I am currently only able to get the overall window height. I'm not interested in retrieving the ...

How can Node / Javascript import various modules depending on the intended platform?

Is there a way to specify which modules my app should import based on the target platform in TypeScript? I am interested in importing different implementations of the same interface for a browser and for Node.js. In C++, we have something like: #ifdef wi ...

Create a Jest test environment with MongoDB and TypeScript, following the guidance provided in the Jest documentation

While attempting to set up a simple test file following the jest documentation, I encountered several linter errors: connection: The type 'Promise<MongoClient> & void' is missing properties such as '{ db: (arg0: any) => any; cl ...

Getting object arguments from an npm script in a NodeJS and TypeScript environment can be achieved by following these steps

I'm trying to pass an object through an NPM script, like this: "update-user-roles": "ts-node user-roles.ts {PAID_USER: true, VIP: true}" My function is able to pick up the object, but it seems to be adding extra commas which is ...

Synchronizing Form Data in Angular 5: Pass and Populate Dropdowns between Components

I have developed a unique form (material dialog modal) that allows users to create an account. When the user clicks on the Register button, their created account should appear in a dropdown menu without redirecting or reloading the current page. I am facin ...

What is the best way to export a ReactTS component along with its children?

After importing the component, I proceed to declare a new component which will be a child for the invoked one. import { someComponent } from './someComponent'; This is how I export it: const anotherComponent = () => {...}; export { someCompon ...