Creating a custom function in TypeScript to redefine the functionality of the `in` operator

I am looking to create a custom function that mimics the behavior of the in operator in TypeScript, utilizing user-defined type guards.

(To see an example, check out Lodash's has function.)

When using n in x where n is a string literal or string literal type, and x is a union type, the "true" branch narrows down to types with either an optional or required property n, while the "false" branch narrows down to types missing that property n.

https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-the-in-operator

I have written tests demonstrating the behavior of the in operator and want to replicate this behavior in my has function. How can I define the function so it behaves exactly like the in operator based on these tests?

declare const any: any;

type Record = { foo: string; fooOptional?: string };
type Union = { foo: string; fooOptional?: string } | { bar: number; barOptional?: string };

{
  const record: Record = any;

  if ('foo' in record) {
    record; // $ExpectType Record
  } else {
    record; // $ExpectType never
  }

  if (has(record, 'foo')) {
    record; // $ExpectType Record
  } else {
    record; // $ExpectType never
  }
}

{
  const union: Union = any;

  if ('foo' in union) {
    union; // $ExpectType { foo: string; fooOptional?: string | undefined; }
  } else {
    union; // $ExpectType { bar: number; barOptional?: string | undefined; }
  }

  if (has(union, 'foo')) {
    union; // $ExpectType { foo: string; fooOptional?: string | undefined; }
  } else {
    union; // $ExpectType { bar: number; barOptional?: string | undefined; }
  }
}

{
  const unionWithOptional: { foo: string } | { bar?: number } = any;

  if ('bar' in unionWithOptional) {
    unionWithOptional; // $ExpectType { bar?: number | undefined; }
  } else {
    unionWithOptional; // $ExpectType { foo: string; } | { bar?: number | undefined; }
  }

  if (has(unionWithOptional, 'bar')) {
    unionWithOptional; // $ExpectType { bar?: number | undefined; }
  } else {
    unionWithOptional; // $ExpectType { foo: string; } | { bar?: number | undefined; }
  }
}

The solution I've attempted for now can be found here:

type Discriminate<U, K extends PropertyKey> = U extends any
  ? K extends keyof U
    ? U
    : U & Record<K, unknown>
  : never;

export const has = <T extends object, K extends PropertyKey>(
  source: T,
  property: K,
): source is Discriminate<T, K> =>
  property in source;

Unfortunately, the last test does not pass as expected.

In essence, my goal is to ensure that my custom has function validates keys at compile time, which the traditional in operator falls short on. While I can handle the validation aspect, I'm currently struggling with replicating the narrowing effect carried out by the in operator.

Answer №1

At this time, without specific type guards that cater to both true and false scenarios as discussed in microsoft/TypeScript#14048, achieving independent behaviors for each side of the guard is not feasible.


Your request involves creating a user-defined type guard for a union type

{ foo: string } | { bar?: number }
where the true branch refines it to { bar?: number } while the false branch maintains the original union type
{ foo: string } | { bar?: number }
.

Upon analyzing the getNarrowedType() function from lines 19788 to 19809 of checker.ts, which is the TypeScript compiler's type checker, it appears unlikely to achieve such behavior.

Currently, when applying a user-defined type guard of type (x: any) => x is G to a value of union type T, the compiler segregates the union type for the true and false branches of the guard. This results in narrowing to Extract<T, G> for the true branch and Exclude<T, G> for the false branch. Each branch will exclude members present in the other branch.

Unless T is not a union type, it is difficult to have overlapping narrowings for the true and false branches (with possibilities like T & G for the true branch and simply T for the false branch).

While not ideal (as mentioned in ms/TS#31156), this is the current limitation.

Answer №2

The function called 'has' has the capability to narrow down the type of its argument, specifically when dealing with optional keys that can be narrowed as non-optional:

function has<T,K extends keyof T>(obj: T, key: K):
    obj is T & Record<K, Exclude<T[K], undefined>>
{
    return obj[key] !== undefined;
}

type Example = { a: number, b?: number };
let obj: Example = { a: 1, b: 2 };

// inferred obj.b : number|undefined

if(has(obj, 'b')) {
    // inferred obj.b : number
}

However, using the 'Union' type in the way you proposed is not valid. Here's an example to illustrate this point:

type UnsoundUnion = { a: number, b: number } | { c: number, d: number };

let u: UnsoundUnion = { a: 1, c: 2, d: 3 };

if(has(u, 'a')) {
    // inferring u.b as number would be incorrect
}

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 it possible to use Date as a key in a Typescript Map?

Within my application, I have a requirement for mapping objects according to specific dates. Given that typescript provides both the Map and Date objects, I initially assumed this task would be straightforward. let map: Map<Date, MyObject> = new M ...

Issue with Docker setup for managing a PNPM monorepo

As a newcomer to Docker, I am attempting to configure my fullstack API REST project. Within my PNPM workspace, I have two applications - a frontend built with Angular and a backend developed using AdonisJS. My goal is to create a Docker configuration for P ...

Experiencing a freeze in WebStorm 11 when trying to navigate to the declaration of Typescript interfaces

As I work on a massive NodeJS/Typescript project in WebStorm 11, I frequently experience complete freezing when using the "Go To -> Declaration" shortcut. This forces me to manually kill and restart WebStorm. Have any of you experienced this issue as w ...

What's the best way to include various type dependencies in a TypeScript project?

Is there a more efficient way to add types for all dependencies in my project without having to do it manually for each one? Perhaps there is a tool or binary I can use to install all types at once based on the dependencies listed in package.json file. I ...

Leveraging TypeScript global variables in SharePoint operations

In TypeScript and Angular, I've developed a function called getTasks() that should run when a modal is closed. Here's the code for the function: getTasks() { this.http.get(`https://example.com/_api/web/lists/getbytitle('Tasks')/items` ...

Exploring the Contrast in Typescript: Static Members within Classes vs Constants within Namespace

When creating this namespace and class in Typescript, what distinguishes the two? How does memory management vary between them? export namespace TestNamespace { export const Test = 'TEST'; export const TestFunction = (value:string):boolean = ...

The issue with Angular2 Material select dropdown is that it remains open even after being toggled

Exploring the world of Node.js, I am delving into utilizing the dropdown feature from Angular Material. However, an issue arises once the dropdown is opened - it cannot be closed by simply clicking another region of the page. Additionally, the dropdown lis ...

Receiving an `Invalid module name in augmentation` error is a common issue when using the DefaultTheme in Material

After following the guidelines outlined in the migration documentation (v4 to v5), I have implemented the changes in my theme: import { createTheme, Theme } from '@mui/material/styles' import { grey } from '@mui/material/colors' declar ...

Organizing data in TypeScript

Is there a way to alphabetically sort this list of objects by name using TypeScript? "[{name:"Prasanna",age:"22",sex:"Male",Designation:"System Engineer",Location:"Chennai"}, {name:"Nithya",age:"21",sex:"Female",Designation:"System Engineer",Location ...

What is the process of overriding a method from an abstract class that employs generics for typing?

In my quest to develop an abstract class with an abstract static method, I find myself wanting to override this method in a concrete class. The static nature of the method stems from its responsibility to create a 'copy' from a database model and ...

What is the best way to implement event handling for multi-level components/templates in Angular 5?

Currently, I am immersed in a project using Angular 5. One of the templates, called Template A, is filled with various HTML elements. I am incorporating Template A into another template, Template B, which offers additional functionalities on top of Templat ...

Encountering a Problem on Heroku: TypeScript Compilation Error with GraphQL Files - TS2307 Error: Module 'graphql' Not Found or Its Type Declarations Missing

While trying to compile my typescript project with graphql files for deployment on Heroku, I encountered the following error message: node_modules/@types/graphql-upload/index.d.ts(10,35): error TS2307: Cannot find module 'graphql' or its correspo ...

What is the correct way to interpret JSON data received from an Ajax response?

Here is the code snippet that I used: axios.get(url).then(result => { this.setState({ list: result.data, }); }) .catch(e => { console.log("Some error occurred", e); }); The constructor in my code looks like this: constructor(pro ...

Looking for giphy link within a v-for loop (Vue.js)

I am fetching a list of movie characters from my backend using axios and rendering them in Bootstrap cards. My objective is to search for the character's name on Giphy and use the obtained URL as the image source for each card. However, when I attemp ...

AWS Lambda Layer - Typescript - Error Importing Module Runtime

Hello there! I'm currently facing an issue with implementing lambda layers on a lambda function that I've set up. I've tried numerous approaches to get it right, but nothing seems to be working. Based on my research, this seems to be a comm ...

Leverage and implement a reusable class in Typescript

In a React Typescript project, I am facing a challenge. I want to utilize a base class component and add an additional property to its State. After attempting to modify the class from class ErrorBoundaryW extends PureComponent<any, State> {...} to ...

Tips for addressing the issue - error TS2451: Unable to redefine block-specific variable 'total'

I am currently working on setting up the Jest testing framework and running a basic test (using an example from jestjs.io). Here is the content of the sum.ts file: function sum(a: any, b: any):any { return a + b; } module.exports = sum; And here is the ...

Observable doesn't respond to lazy loaded module subscriptions

I am trying to understand why my lazy loaded module, which loads the test component, does not allow the test component to subscribe to an observable injected by a test service. index.ts export { TestComponent } from './test.component'; export { ...

Error message 2339 - The property 'toggleExpand' is not recognized on the specified type 'AccHeaderContextProps | undefined'

When utilizing the context to share data, I am encountering a type error in TypeScript stating Property 'toggleExpand' does not exist on type 'AccHeaderContextProps | undefined'.ts(2339). However, all the props have been declared. inter ...

VPS mysteriously terminates TypeScript compilation process without any apparent error

I've encountered an issue while trying to compile my TypeScript /src folder into the /dist folder. The process works smoothly on my local machine, but when I clone the repo onto my VPS (Ubuntu Server 22.04), install npm, and run the compile script, it ...