What is the best way to refine object T types by supplying an array of exclusions (keyof T)[]?

What's the best way to create a type guard that can exclude keys from an object in TypeScript?

Below is my getExcludedKeys function, which aims to filter out certain keys from an object. However, I'm encountering an issue where the type guard isn't narrowing down the type as expected, and I end up with all 3 properties instead of just the ones I want to exclude.

I've used type guards in filter expressions before, but never with a predefined array like this.

interface Foo {
  id: number
  val1: string
  val2: string
}

type KeysOf<T> = (keyof T)[]
const getKeys = <T> (obj: T) => Object.keys(obj) as KeysOf<T>
const getExcludedKeys = <T> (obj: T, excludeKeys: KeysOf<T>) =>
  getKeys(obj)
    .filter((key): key is Exclude<keyof T, typeof excludeKeys> => { // This line isn't working as expected.
      return !excludeKeys.includes(key)
    })


const foo: Foo = {
  id: 1,
  val1: 'val1',
  val2: 'val2'
}

const result = getExcludedKeys(foo, ['val1', 'val2'])
  .map(key => key)  // EXPECTED :: key: "id"
                    // ACTUAL   :: key: "id" | "val1" | "val2"

Answer №1

You wrote a self-answer, put a bounty on your question, and included this comment:

If the array of excluded keys is defined in a variable/constant before being passed to exclude, it causes issues...

It seems like the problem you're facing is related to this. When you modify your code like this, an error occurs on the second argument of exclude:

const toExclude = ["foo", "bar"];
const b = exclude(a, toExclude)
    .map(key => {                   
        if (key === 'bar') { }     
        if (key === 'foo') { }      
        if (key === 'baz') { }      
        if (key === 'yay') { }      
    });

The issue arises because when you declare an array like this:

const toExclude = ["foo", "bar"];

TypeScript infers the "best common type," which is the most common type among the elements. If you want a narrower type, you must be explicit. In your case, where the array should contain a subset of the keys from the first argument of exclude, you need to specify the type:

const toExclude = ["foo", "bar"] as ["foo", "bar"];

To avoid repeating this type assertion for multiple arrays, you can use a helper function like:

function asKeysOf<T extends (keyof typeof a)[]>(...values: T) { return values; }

const toExclude = asKeysOf("foo", "bar");

Another method that works with any object type is:

function asKeysOf<O, T extends (keyof O)[]>(o: O, ...values: T) { return values; }

const toExclude = asKeysOf(a, "foo", "bar");

Be aware that TypeScript 3.4 introduces a feature where you can infer the narrowest type for literal values using as const. For example:

const toExclude = ["foo", "bar"] as const;

In this case, ensure the exclude function expects a readonly array for excludes:

function exclude<T, K extends readonly (keyof T)[]>(obj: T, excludes: K) {

Here's a revised example compatible with TypeScript 3.4:

function getAllKeys<T>(obj: T): (keyof T)[] {
  return Object.keys(obj) as (keyof T)[];
}

function exclude<T, K extends readonly (keyof T)[]>(obj: T, excludes: K) {
    return getAllKeys(obj)
        .filter((key: keyof T): key is Exclude<keyof T, K[number]> =>
                !excludes.includes(key));
}

const a = {
    foo: 'abc',
    bar: 'abc',
    baz: 'abc',
    yay: true
};

const toExclude = ["foo", "bar"] as const;

const b = exclude(a, toExclude)
    .map(key => {                   
      if (key === 'bar') { }      
      if (key === 'foo') { }      
      if (key === 'baz') { console.log("Q"); }      
      if (key === 'yay') {console.log("F"); }      
    });

This example works as expected with TypeScript 3.4.0-rc.

Answer №2

After hours of experimenting, I finally cracked the code with a bit of assistance from @nucleartux and his Omit type.

All it took was this unconventional type

Omit<T, K extends KeysList<T>> = Exclude<keyof T, K[number]>
used as a type guard along with exclude having a second generic type K extends (keyof T)[]

type KeysList<T> = (keyof T)[]
type Omit<T, K extends KeysList<T>> = Exclude<keyof T, K[number]>

function getAllKeys<T>(obj: T): KeysList<T> {
    return Object.keys(obj) as KeysList<T>
}

function exclude<T, K extends KeysList<T>>(obj: T, excludes: K) {
    const filterCallback = (key: keyof T): key is Omit<T, K> => // <-- THIS LINE
        !excludes.includes(key) 

    return getAllKeys(obj)
        .filter(filterCallback)
}

const a = {
    foo: 'abc',
    bar: 'abc',
    baz: 'abc',
    yay: true
};

const b = exclude(a, ['foo', 'bar'])
    .map(key => {                   // (EXPECTED :: key: 'baz') (ACTUAL :: key: 'foo' | 'bar' | 'baz')
        if (key === 'bar') { }      // Error
        if (key === 'foo') { }      // Error
        if (key === 'baz') { }      // Okay
        if (key === 'yay') { }      // Okay
    });

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

I need to compile a comprehensive inventory of all the publicly accessible attributes belonging to a Class/Interface

When working with TypeScript, one of the advantages is defining classes and their public properties. Is there a method to list all the public properties associated with a particular class? class Car { model: string; } let car:Car = new Car(); Object. ...

Material-UI Slide component is encountering an issue where it is unable to access the style property of an undefined variable

Recently, I incorporated Material-UI Slide into my project and I'm curious about why the code functions correctly when written in this manner: {selectedItem && selectedItem.modal && selectedItem.modal.body ? ( selectedItem.modal.body.map((section ...

What is the method for retrieving data from a node in Firebase Realtime Database using TypeScript cloud functions, without relying on the onCreate trigger?

Being a beginner with Firebase and TypeScript, I have been struggling to retrieve values from a reference other than the triggered value. Despite finding answers in JavaScript, I am working on writing functions using TypeScript for real-time database for A ...

A guide on efficiently utilizing combineLatest and mergeMap for handling multiple subscriptions

As I continue to delve into the world of rxjs, I've encountered an issue with managing multiple subscriptions. Specifically, I'm struggling to extract an ID from a response in order to correctly associate photos with products. create(product) { ...

The improved approach to implementing guards in Angular

I am currently looking for the most effective way to utilize Angular "guards" to determine if a user is logged in. Currently, I am checking if the token is stored. However, I am wondering if it would be better to create an endpoint in my API that can verif ...

Is it possible for Go's http server to compile TypeScript?

My current setup involves a NodeJS Application that launches an http server and utilizes client side code written in TypeScript with Angular 2. I'm curious if it's possible to achieve the same functionality using Go programming language? I trie ...

Using Angular: filtering data streams from a date range observable object

I have a piece of code that seems to be functioning correctly, but I can't shake the feeling that it might just be working by chance due to an undocumented feature. I'm torn between questioning its validity or accepting that it is indeed designed ...

Angular is giving me an undefined Array, even though I clearly defined it beforehand

I've been working on integrating the Google Books API into my project. Initially, I set up the interfaces successfully and then created a method that would return an Array with a list of Books. public getBooks(): Observable<Book[]>{ ...

Exploring Angular: Looping through an Array of Objects

How can I extract and display values from a JSON object in a loop without using the keyValue pipe? Specifically, I am trying to access the "student2" data and display the name associated with it. Any suggestions on how to achieve this? Thank you for any h ...

Dealing with numerous dynamically generated tables while incorporating sorting in Angular: a comprehensive guide

I am faced with a challenge involving multiple dynamically created Angular tables, each containing the same columns but different data. The issue at hand is sorting the columns in each table separately. At present, I have two tables set up. On clicking the ...

What is the reason for the lack of functionality of the "unique" field when creating a schema?

I've created a schema where the username field should be unique, but I'm having trouble getting it to work (The "required" constraint is functioning correctly). I've tried restarting MongoDB and dropping the database. Any idea what I might b ...

Accessing the value of a FormControl in HTML代码

Modifying the value of a form select element programmatically presents an issue. Even after changing the value in the form, the paragraph element "p" remains hidden. However, if you manually adjust the form's value, the visibility of the "p" element ...

Obtain the parameters of a function within another function that includes a dynamic generic

I am attempting to extract a specific parameter from the second parameter of a function, which is an object. From this object, I want to access the "onSuccess" function (which is optional but needed when requested), and then retrieve the first dynamic para ...

Error: The current call does not match any existing overloads - TypeScript, NextJS, styled-components

I'm facing an issue while trying to display icons in the footer of my website. The error message "Type error: No overload matches this call" keeps appearing next to my StyledIconsWrapper component, causing Vercel deployment to fail. Despite this error ...

Is it possible to configure npm to publish to an organization different from the one automatically detected from package.json?

We are looking to implement a process in our open source project where all Pull Requests will be published to npm using CI/CD. To reduce the potential for supply chain attacks, we aim to deploy to a separate organization. Can this be achieved without makin ...

How can I access keys and values from an Observable within an Angular template?

Attempting to display all keys and values from an Observable obtained through Angular Firebase Firestore Collection. This is how I establish a connection to the collection and retrieve an Observable. The function is called subsequently. verOrden : any; ...

Displaying a segment of information extracted from a JSON array

I'm currently tackling a project that involves using React, Redux, and TypeScript. Within the JSON file, there is a key-value pair: "data_start": "2022-09-02" Is there a way to display this date in a different format, specifical ...

Alias route for `src` in Ionic 3

I have set up a custom webpack configuration for Ionic 3 in order to use src as a path alias (meaning I can import from src/module/file): resolve: { alias: { 'src': path.resolve('./src') } } However, after updating to Ionic ap ...

Using static methods within a static class to achieve method overloading in Typescript

I have a TypeScript static class that converts key-value pairs to strings. The values can be boolean, number, or string, but I want them all to end up as strings with specific implementations. [{ key: "key1", value: false }, { key: "key2&qu ...

Issue encountered with connecting to development server on Expo iOS simulator that is not present when using a browser

During the development of a chat application with React Native Expo, I encountered an issue when running "expo start" in my typical workflow. The error message displayed was "could not connect to development server." If anyone has experienced a similar pr ...