Complete set of keys within a type

In my TypeScript project, I am working with an API (specifically OData API) to retrieve data.

This API allows the selection of specific fields to retrieve. For example,

api/some/getbyid(42)?$select=x,y,z
can be used to get fields x, y, and z, along with some default technical fields such as id or author.

I have model types that correspond to the data retrieved from the API:

type APIItem = {
  id : number; 
  author : string; 
}

type CustomerModel = APIItem  & {
  firstName : string;
  lastName:string;
  age : number
}

I encapsulate the data retrieval logic in a function that takes the ID and fields to be retrieved, makes the API call, and processes the result:

const fakeApi = (id: number, fields : string[]): Promise<APIItem> => {
  // Construct API URL, fetch data, etc.
  const result = JSON.parse(`{
    "id": ${id}, "firstName": "Mark", "lastName": "Hamill", "age": 20, "author": "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="83e7c3f0eceee6ade0ecee">[email protected]</a>"
  }`) ;
  return Promise.resolve(result);
}

const loadFromDB = async <
TModel extends APIItem
>(
  id: number, // Item's ID in the API
  fields : (keyof Omit<TModel, 'id'> & string)[] // Fields to retrieve. Exclude ID because it is always included.
  ): Promise<TModel> => {

  const fromDb = await fakeApi(id, fields);
  const result = fromDb as TModel;
  return Promise.resolve(result);
}

const customerPromise = loadFromDB<CustomerModel>(42, ['firstName']); // Error in code: missing fields
customerPromise.then(console.log).catch(console.error);

The functionality works as expected except for one issue: the consumer code must specify all fields to retrieve. In the example above, only the firstName field is retrieved, resulting in an incomplete object.

Is there a simple way to ensure that all fields from the model are provided in the API call?

As far as I know, TypeScript does not offer a way to iterate over keys in a type since types do not translate to actual JavaScript output.

I would like to enforce that the calling code specifies ALL fields (or allow the function to determine the necessary fields on its own):

loadFromDB<CustomerModel>(42, ['firstName', 'lastName', 'age']);

This approach ensures that the models will always be complete.

Playground Link

Answer №1

When I need both runtime and compile-time information, my approach involves deriving compile-time information from the data used for runtime purposes. Usually, it's not possible to go the other way around, except in special cases like enums. "Exhaustive arrays," as described, can pose challenges - see example by user123.

To achieve this, consider defining a CustomerModel based on a CustomerModelFields object containing field names and placeholder values:

// Main source of truth: object with field names and representative values
const CustomerModelFields = {
    firstName: "",
    lastName: "",
    age: 0,
};

// Model derived from fields object
type CustomerModel = APIItem & typeof CustomerModelFields;

Omitting author (absent from your sample call) and treating id as part of APIItem, modify the existing code:

In the modified version, loadFromDB takes the fields object type as its parameter type and leverages it for fields (an object rather than an array), determining its return type based on the fields object:

const loadFromDB = async <Fields extends object>(
    id: number, // Item ID
    fields: Fields // Fields to retrieve (excluding Id)
): Promise<APIItem & Fields> => {
    const fromDb = await fakeApi(id, Object.keys(fields));
    const result = fromDb as APIItem & Fields;
    return Promise.resolve(result);
};

During invocation, specify only the fields rather than both the type and fields:

const customer = await loadFromDB(42, CustomerModelFields);

If you desire the specific alias (CustomerModel) for customer, define the constant as follows:

const customer: CustomerModel = await loadFromDB(42, CustomerModelFields);

Complete example available at the playground link https://www.typescriptlang.org/play?jsx=0&pretty=true#code/C4TwDgpgBAggCgSQcCBbKBeKBvAUFAqASwBMAuKAOwFdUAjCAJwG59CBDa4ACwHtGKAZ2CMilAOasAvq1wB6OVDijU7RiCiDe1RgGNovAGZQRXbsUFR2lKLzoArCLuBQA7kR4nu0Su1QRLIy9oQyIIABsSS2sSKEYIMHjBCEpgdmAiADdoahpk2Mz2cOoA3F1eSmEoAGFqYV5-RgBZXhIIgDEwyMssPEIoUMZhADk-CAoAIgmAGjYCcPYRscmZuatxcagABlmZXHlFFrbwiyg20WzYw0YG4IGuqNsHJ2BcUEgauuAGpiOIzFgiGQaCgADITOAIEFavVGn9wp0IlFZOVKi5DOwANYQGBgIgAgAUpAoNHoTGm9yRgiEIjE4gA2gBdACUFGUDSIyQAPPAkChUAA...:

type APIItem = {
    id: number;
    author: string;
};

// Main source of truth: object with field names and representative values
const CustomerModelFields = {
    firstName: "",
    lastName: "",
    age: 0,
};

// Model derived from fields object
type CustomerModel = APIItem & typeof CustomerModelFields;

// Mock API response
const fakeApi = (id: number, fields: string[]): Promise<APIItem> => {
    const result = JSON.parse(`{
        "id": ${id},
        "firstName": "John",
        "lastName": "Doe",
        "age": 25,
        "author": "john.doe@example.com"
    }`);
    return Promise.resolve(result);
};

// Updated loadFromDB function
const loadFromDB = async <Fields extends object>(
    id: number,
    fields: Fields
): Promise<APIItem & Fields> => {
    const fromDb = await fakeApi(id, Object.keys(fields));
    const result = fromDb as APIItem & Fields;
    return Promise.resolve(result);
};

// Invoke loadFromDB asynchronously
(async () => {
    try {
        const customer = await loadFromDB(42, CustomerModelFields);
        console.log(customer);
    } catch (error: any) {
        console.error;
    }

    // Alternatively, use the alias
    try {
        const customer: CustomerModel = await loadFromDB(42, CustomerModelFields);
        console.log(customer);
    } catch (error: any) {
        console.error;
    }
})();

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

typescript - add a global library import statement to every script

Recently, I began working on a TypeScript project in Node.js. I am looking to add import 'source-map-support/register'; to all of my .ts files for better visibility of TS source in stack traces. Is there a method to implement this without manuall ...

Piping in Angular 2 with injected dependencies

Is it possible to inject dependencies such as a service into Angular 2 pipes? import {Pipe, PipeTransform} from 'angular2/core'; import {MyService} from './service'; //How can I inject MyService into the pipe? @Pipe({name: 'expo ...

Tips for transforming Http into HttpClient in Angular 5 (or higher than 4.3)

I have successfully implemented code using Http and now I am looking to upgrade it to use the latest HttpClient. So far, I have taken the following steps: In App.module.ts: imported { HttpClientModule } from "@angular/common/http"; Added HttpClientModul ...

Toggle the presence of a string in an array with a checkbox

Currently, I am working on a user creation form for my Next.js front end project using TypeScript. The main goal is to allow an administrator to create new users by filling out a simple form which will generate a basic user object. Here is the structure of ...

An issue arising with the TypeScript antlr4ts listener type

I am currently attempting to incorporate the antlr4 parser into an angular project. Within a dataservice class, there is a function being called that appears as follows: parseRule() { const ruleString = ' STRING TO PARSE'; const inputS ...

Error: The code is unable to access the '0' property of an undefined variable, but it is functioning properly

I am working with two arrays in my code: bookingHistory: Booking[] = []; currentBookings: any[] = []; Both arrays are populated later in the code. The bookingHistory array consists of instances of Booking, while currentBookings contains arrays of Booking ...

Having trouble retrieving a value from a reference object in React Typescript?

Struggling with a login form issue in my React TypeScript project. Below is the code for the react login form: login-form.tsx import * as React from 'react'; import { Button, FormGroup, Input, Label } from 'reactstrap' ...

TypeScript - Indexable Type

Here is an explanation of some interesting syntax examples: interface StringArray { [index: number]: string; } This code snippet defines a type called StringArray, specifying that when the array is indexed with a number, it will return a string. For e ...

Adding a second interface to a Prop in Typescript React: a step-by-step guide

import { ReactNode, DetailedHTMLProps, FormHTMLAttributes } from "react"; import { FieldValues, SubmitHandler, useForm, UseFormReturn, } from "react-hook-form"; // I am looking to incorporate the DetailedHTMLProps<FormHTMLAt ...

Updating the navigation bar in Node/Angular 2 and displaying the sidebar once the user has logged in

I am facing a challenge with my first project/application built using Angular 2, particularly related to the login functionality. Here is what I expect from the application: Expectations: When I load the login component for the first time, the navbar ...

The 'toDataURL' property is not recognized on the 'HTMLElement' type

Hey there! I'm new to TypeScript and I've been experimenting with generating barcodes in a canvas using JSBarcode and then adding it to JSpdf as an image using the addImage function. However, I keep encountering the error mentioned above. barcod ...

Looking for assistance in setting up a straightforward TypeScript Preact application

I recently started exploring preact and I'm attempting to create a basic app using typescript in preact. I've noticed that their default and typescript templates include extras like jest and testing, which I don't necessarily require. Althou ...

The module 'Express' does not have a public member named 'SessionData' available for export

I am encountering an issue while working on my TypeScript project. I am not sure where the error is originating from, especially since nothing has been changed since the last time I worked on it. node_modules/connect-mongo/src/types.d.ts:113:66 - error TS ...

Implementing basic authentication with AWS Lambda and Application Load Balancer

A few days ago, I sought assistance on implementing basic-authentication with AWS Lambda without a custom authorizer on Stack Overflow. After receiving an adequate solution and successfully incorporating the custom authorizer, I am faced with a similar cha ...

Checking at compile time whether a TypeScript interface contains one or multiple properties

Is there a way to determine if a typescript interface contains at least one property at compile time without knowing the property names? For example, with the interfaces Cat and Dog defined as follows: export type Cat = {}; export type Dog = { barking: bo ...

Encountered an issue in Angular 2 when the property 'then' was not found on type 'Subscription'

I have been attempting to call a service from my login.ts file but I am encountering various errors. Here is the code snippet in question: login.ts import { Component } from '@angular/core'; import { Auth, User } from '@ionic/cloud-angular ...

Angular2: Unable to locate the 'environment' namespace

After configuring my tsconfig.json, I can now use short import paths in my code for brevity. This allows me to do things like import { FooService } from 'core' instead of the longer import { FooService } from '../../../core/services/foo/foo. ...

When trying to reload Angular 8 pages, encountering an error that reads "Cannot GET /login" and also receiving a notification stating the image '/favicon.ico' cannot be loaded due to a violation of

Encountering an issue with the error message "Cannot GET login/" appearing on the page body of my latest Angular 8 app. Despite attempting various solutions found on forums, I have been unable to resolve this error. Any suggestions or advice would be great ...

Adding a line break ( ) in a paragraph within a TypeScript file and then transferring it to HTML does not seem to be functioning properly

Angular Website Component: HTML file <content-section [text]="data"></content-section> TypeScript file data = `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's stand ...

I will evaluate two arrays of objects based on two distinct keys and then create a nested object that includes both parent and child elements

I'm currently facing an issue with comparing 2 arrays of objects and I couldn't find a suitable method in the lodash documentation. The challenge lies in comparing objects using different keys. private parentArray: {}[] = [ { Id: 1, Name: &ap ...