Find non-null values inferred from a string identifier in Typescript

Imagine having a dynamic object with various keys and values, where the keys can be of any type, including null and undefined.

Now, consider a function that takes an array of string paths to values within this object and returns true only if all those values exist (i.e., are not null or undefined).

Is there a way for TypeScript to automatically infer this information so that we don't have to rely on the ! operator:

interface Foo {
  a?: { i?: number; ii?: number },
  b?: string;
  c?: {}
}
var object: Foo = {
    a: {
        i: undefined,
        ii: 1
    },
    b: undefined,
    c: {}
}

if (exists(object, ["a.ii", "c"])) // should return true only if object.a.ii and object.c are not null or undefined
{
    // TypeScript should infer that object.a.ii is not null or undefined
    // So that we can avoid using the '!' operator
    let value: number = object.a.ii!;
}

We can achieve a similar result using the following approach:

function exists<T>(object: T | null | undefined): object is T
{
    return object !== null && object !== undefined;
}

if (exists(object.a.ii) && exists(object.c))
{
    let value: number = object.a.ii; // No need for '!' operator
}

However, I'm curious to know if it's possible to accomplish this using string paths instead.

EDIT: Added type to example

Answer №1

Our goal is to transform the exists(obj, paths) function into a specialized user-defined type guard function that operates as a generic. When obj has a generic type of T and paths holds a generic type of K[], we aim for a return value of true to refine obj to a subtype of T with non-non-nullish properties at the specified paths in K.

In order to achieve this, we need to create a variant of the Record<K, V> utility type called NestedRecord<K, V>. This new type should interpret K as a collection of dotted paths rather than plain keys. Here's how it should work:

type Example = NestedRecord<
  "z.y.x" | "z.w.v" | "u.t" | "u.t.s" | "r.q" | "p",
  Date
>;
/* Output: {
  z: {
    y: { x: Date; };
    w: { v: Date; };
  };
  u: { t: Date & { s: Date; }; };
  r: { q: Date; };
  p: Date;
}*/

For accurate representation, we want the call signature of exists() to be structured like this:

declare function exists<T extends object, K extends string>(
  obj: T, paths: K[]
): obj is T & NestedRecord<K, {}>;

Now our attention shifts towards defining NestedRecord<K, V>.


We can implement it using a technique that involves key remapping in mapped types along with template literal types to disassemble the keys within K. The keys within NestedRecord<K, V> should represent segments before the initial dots in K, or any sections of K lacking dots. Similarly, the values enclosed in NestedRecord<K, V> should correspond to V for dotless parts of

K</code, aligned with <a href="https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types" rel="nofollow noreferrer">intersections</a> with <code>NestedRecord<K, V>
for segments in K post the primary dot associated with the present key.

Note that NestedRecord<never, V> needs to translate to unknown instead of {} to eliminate inconveniences stemming from numerous intersections with {} in the final type.

A test on the definition proves its intent with the provided Example.


To put exists() into practice, here's one conceivable approach:

function exists<T extends object, K extends string>(
  obj: T, paths: K[]
): obj is T & NestedRecord<K, {}> {
  return paths.every(path => path.split(".").reduce((accumulator, key) => (accumulator ?? accumulator[key]), obj) != null);
}

The utilization of the every() and reduce() array methods ensures validation of non-nullish values within object across all paths prescribed by paths.


An application of this function on an illustrative object manifest itself as follows:

object;
// ^? var object: Foo
if (exists(object, ["a.ii", "c"])) {
  object;
  // ^? var object: Foo & { a: { ii: {}; }; c: {}; }
  let value = object.a.ii;
  // ^? let value: number
  console.log(value); // 1
}

The success outcome showcases effective narrowing of object from Foo to

Foo & { a: { ii: {}; }; c: {}; }
. Consequently, exploration of object.a.ii after narrowing reflects a type equivalent to (number | undefined) & {}, which simplifies to number, fulfilling the defined criteria.

Playground link to code

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

Is there a way to dynamically create a property and assign a value to it on the fly?

When retrieving data from my API, I receive two arrays - one comprising column names and the other containing corresponding data. In order to utilize ag-grid effectively, it is necessary to map these columns to properties of a class. For instance, if ther ...

You cannot assign type void to type any

I'm currently working on a component that involves some code: export class AddNewCardComponent { public concept = []; constructor( private _router: Router, private _empDiscService: empDiscService) { } ngOnIni ...

Conditional void parameter type in Typescript

Attempting to create a custom Error class that can handle different data based on the error code seemed like a complex task for TypeScript. However, surprisingly, it was successful: const enum ERROR_CODES { E_AUTHORIZATION = 'Authorization error&ap ...

What is the best way to retrieve a {collection object} from a JavaScript map?

My application utilizes a third-party library that returns the map in the following format: public sids: Map<SocketId, Set<Room>> = new Map(); When I try to access it using the code below: io.of("/").adapter.sids.forEach(function(va ...

What could be causing the module to break when my Angular service, which includes the httpClient, is added in the constructor?

After creating a backend RESTful API, I encountered difficulties while trying to access it. To address this issue, I developed a database-connection.service specifically for making POST requests. However, I am facing challenges in implementing this solut ...

Issue: The inject() function can only be executed within an injection context. Issue: An untruthy value was expected to be truth

I'm currently in the process of setting up a unit test for my app.component. I've imported all the necessary components, but I keep encountering an error that's puzzling me. I activated "preserveSymlinks": true in my angular.json file, but t ...

When the typeof x is determined to be "string", it does not result in narrowing down to just a string, but rather to T & string

Could someone help me understand why type narrowing does not occur in this specific case, and return typing does not work without using: as NameOrId<T>; Is there a more efficient way to rewrite the given example? Here is the example for reference: ...

The Element type does no feature a Typescript property

Despite my attempts to include a declaration file and various other solutions, I'm still struggling with this issue: The goal is to define the visible property as a function on the HTML Element object. However, the linter keeps flagging visible with ...

Secure higher order React component above class components and stateless functional components

I have been working on creating a higher order component to verify the authentication status of a user. Currently, I am using React 15.5.4 and @types/react 15.0.21, and below is a simplified version of my code: import * as React from 'react'; i ...

What is the most effective method for delivering a Promise after an asynchronous request?

Currently, I am working on creating an asynchronous function in TypeScript that utilizes axios to make an HTTP request and then returns a Promise for the requested data. export async function loadSingleArweaveAbstraction(absId : string) : Promise<Abstra ...

Creating a OneToMany relationship in NestJS entity model

In my current project with NestJS, I am working on defining entity fields. While I have successfully defined a ManyToOne relation, I am facing difficulties in setting up the syntax for a OneToMany relation to match the structure of my other relationships. ...

Having difficulty initializing a new BehaviourSubject

I'm struggling to instantiate a BehaviourSubject I have a json that needs to be mapped to this Typescript class: export class GetDataAPI { 'some-data':string; constructor (public title:string, public description:string, ...

Mastering the process of importing AngularJS submodules in TypeScript

Currently, I am in the process of structuring an AngularJS (Angular 1) project using TypeScript. To compile TypeScript & ES6 to JavaScript, I have set up webpack. In my webpack configuration, I only compile the "app.ts" file and any other files it imports ...

The text "Hello ${name}" does not get substituted with the name parameter right away in the message string

I'm struggling to get the basic TypeScript feature to work properly. Everywhere I look on the Internet, it says that: var a = "Bob" var message = 'Hello ${a}' should result in a console.log(message) printing "Hello Bob". Howeve ...

Creating a generic array type in TypeScript that includes objects with every key of a specified interface

I am working with an interface called Item interface Item { location: string; description: string; } Additionally, I have a generic Field interface interface Field<T extends object> { name: keyof T; label: string; } I want to create an Arra ...

Why are my values not being applied to the model class in Angular 7?

I'm currently developing an online shopping website where I have defined my order Model class as shown below: import { User } from './user.model'; export class Order { constructor(){} amount: Number = 0; status: String = ""; date: ...

Submit information by utilizing' content-type': 'application/x-www-form-urlencoded' and 'key': 'key'

Attempting to send data to the server with a content-type of 'application/xwww-form-urlencode' is resulting in a failure due to the content type being changed to application/json. var headers= { 'content-type': 'applica ...

Innovative Functions of HTML5 LocalStorage for JavaScript and TypeScript Operations

Step-by-Step Guide: Determine if your browser supports the use of localStorage Check if localStorage has any stored items Find out how much space is available in your localStorage Get the maximum storage capacity of localStorage View the amount of space ...

Angular does not seem to support drop and drag events in fullCalendar

I am looking to enhance my fullCalendar by adding a drag and drop feature for the events. This feature will allow users to easily move events within the calendar to different days and times. Below is the HTML code I currently have: <p-fullCalendar deep ...

"Using RxJS to create an observable that filters and splits a string using

I need to break down a string using commas as separators into an observable for an autocomplete feature. The string looks something like this: nom_commune = Ambarès,ambares,Ambares,ambarès My goal is to extract the first value from the string: Ambarès ...