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

Why does the onBlur event function in Chrome but fails to work in Safari?

I've encountered a problem with the onBlur event in react-typescript. To replicate the issue, I clicked the upButton repeatedly to increase the number of nights to 9 or more, which is the maximum allowed. Upon further clicking the upButton, an error m ...

The BooleanField component in V4 no longer supports the use of Mui Icons

In my React-Admin v3 project, I had a functional component that looked like this: import ClearIcon from '@mui/icons-material/Clear' import DoneIcon from '@mui/icons-material/Done' import get from 'lodash/get' import { BooleanF ...

Tips on continuously making calls to a backend API until receiving a successful response with status code 200

While working on my Angular project, I have encountered a situation where I need to make calls to a backend API. If the response is not 200 OK, I have to keep calling the API every 30 seconds until I receive a successful response. In Angular, I usually ca ...

Is it possible for two distinct TypeScript interfaces to share identical keys while allowing for varying values?

Is it possible in TypeScript to ensure that objValidator has the same keys as the obj it validates, with different key values? Any suggestions on how I can achieve this requirement? Specifically, the obj and objValidator should share identical keys. I wan ...

What is the best way to deliver a file in Go if the URL does not correspond to any defined pattern?

I am in the process of developing a Single Page Application using Angular 2 and Go. When it comes to routing in Angular, I have encountered an issue. For example, if I visit http://example.com/, Go serves me the index.html file as intended with this code: ...

Encountering an issue on Safari: WeakMap variable not found in .NET Core 1.1.0 and Angular 2 application

I recently deployed a .NET Core 1.1.0 + Angular 2 + Typescript app on ASPHostPortal and encountered an issue while accessing it from Safari. The console showed the following exception: Can't find variable:WeakMap This caused the site to not load p ...

Having trouble transferring information between two components in Angular version 14

Having recently delved into the world of Angular, I'm grappling with the challenge of passing data from a parent component to a child component. On my product listing page, clicking on a product should route to the product detail page, but I can' ...

Steps for setting up tsconfig.json for Chrome extension development in order to utilize modules:

While working on a Chrome plugin in VS Code using TypeScript, I encountered an issue with the size of my primary .ts file. To address this, I decided to refactor some code into a separate module called common.ts. In common.ts, I moved over certain constan ...

I'm in the process of putting together a node.js project using typescript, but I'm a little unsure about the steps needed to

Currently, I am working on a node.js project that involves compiling with typescript. I recently realized that there is a directory named scripts dedicated to running various tasks outside of the server context, such as seed file operations. With files now ...

Require assistance with accurately inputting a function parameter

I developed this function specifically for embedding SVGs export function svgLoader( path: string, targetObj: ElementRef ){ let graphic = new XMLHttpRequest; graphic.open('GET', path, !0), graphic.send(), graphic.onload = (a)=> ...

Using TypeScript to wrap a class with a Proxy object

I've been working on a function that takes an API interface (I've provided a sample here) and creates a Proxy around it. This allows me to intercept calls to the API's methods, enabling logging, custom error handling, etc. I'm running i ...

Having trouble retrieving JSON file in Next.js from Nest.js app on the local server

Having just started with Next.js and Nest.js, I'm struggling to identify the issue at hand. In my backend nest.js app, I have a JSON API running on http://localhost:3081/v1/transactions. When I attempt a GET request through postman, everything functi ...

Is it possible to globally define a namespace in Typescript?

Seeking a way to make my Input module accessible globally without the need for explicit path definitions. Currently, I have to import it like this: import { Input } from "./Input/Input";. Is there a method to simplify the import statement for modules con ...

Troubleshooting typescript error in styled-components related to Material-UI component

When using typescript and trying to style Material UI components with styled-components, encountering a type error with StyledComponent displaying Type '{ children: string; }' is missing the following properties import React, { PureComponent } f ...

How can you deduce the type from a different property in Typescript?

I have encountered obstacles in my development process and need assistance overcoming them. Currently, I am trying to configure TObject.props to only accept 'href' or 'download' if the condition TObject.name = 'a' is met, and ...

There are no imports in index.js and there is no systemjs configuration set up

After creating a fresh Angular project using ng new some-name, I noticed that the generated index.html file does not include any <script> tags and there is no SystemJS configuration either. Is this the expected behavior? I was anticipating the CLI ...

"Silently update the value of an Rxjs Observable without triggering notifications to subscribers

I'm currently working on updating an observable without alerting the subscribers to the next value change. In my project, I am utilizing Angular Reactive Forms and subscribing to the form control's value changes Observable in the following manner ...

Is there a way to trigger an image flash by hovering over a button using the mouseover feature in AngularJS2?

When I hover over the 'click me' button, I want an image to appear on the webpage. When I stop hovering, the image should disappear using the mouseover option. This is what I attempted in my app.component.ts and my.component.ts files: Here is t ...

What is the connection between tsconfig.json and typings.json files?

I recently acquired a .NET MVC sample application that came with Angular2-final. Within the project, I noticed a typings.json file at the root and a tsconfig.json file in the ng2 app directory. What is the connection between these two files? Is this the mo ...

Angular loop is unable to detect changes in the updated list

My Angular application is facing a peculiar issue that I can't seem to figure out. // Here are the class attributes causing trouble tenants: any[] = []; @Input() onTenantListChange: EventEmitter<Tenant[]>; ngOnInit(): void { this. ...