Generics-based conditional type that verifies entry properties

I developed a function that accepts an argument with two different architectures. I intentionally avoided enforcing rules to allow flexibility for the user, which is now causing me some headaches 😀. Oh well.

My goal is to verify if the entry has a function at the first level. For example:

// Function at the first level

const entry = {
  data: 10,
  upload:() => {},
  otherData: {},
  download: () => {}
}

// Function at a deep level
const entry = {
  photo: {
   total: 10,
   upload: () => {}
  },
  videos: {
    total: 10,
    upload:() => {}
  }
}

If TypeScript detects a function at the first level, I want it to return the type "slice", even if there are functions present in deeper levels. As soon as it finds a function at the first level, it should stop checking and return "slice".

If no function is found at the first level, it should return "group" without checking any further levels.

Note: The return value should be either slice or group, not a combination of both like slice | group.

slice should be returned if a function is found at the first level; otherwise, return group.

The closest solution to passing all my test cases, except one, was to reverse my check like this:

type GetType<S, k = keyof S> = S[k & keyof S] extends Exclude<S[k & keyof S],Function> ?  "group" : "slice"

It works fine but fails when an empty object property is encountered, returning group:

// ❌ This fails and returns 'group' as the type. Expected type is 'slice'
const entry = {
  data: {} // empty object,
  upload: () => {}
}

// ✅ This works and returns 'slice' as expected
const entry = {
  data: any value // empty object,
  upload: () => {}
}

So, we need to find a way to exclude {}, which leads to all the problems. Maybe I'm just tired and can't see clearly 😀. Anyway, I need a fresh perspective to help me out.

Playground link

Answer №1

When it comes to narrowing down the properties of an object type to focus on a specific subset, using a mapped type for filtering is usually the way to go.

In general, it's best to avoid comparing large, complex object types directly and instead focus on comparing smaller, individual components. This approach makes debugging easier as even small differences matter.

My suggestion is to follow a more verbose but accurate strategy. Start by identifying all function properties, and then check if any have been detected.

So, starting with step 1:

type KeysWithFunctions<T> = {
    [K in keyof T]:
        T[K] extends (...args: unknown[]) => unknown
            ? K
            : never
}[keyof T]

This generates a type that combines the names of properties that are functions. If no matching properties are found, the resulting union has no members, represented by never.

For example:

type A = { a: 123, b: () => void, c: () => boolean } // 'a' | 'b'
type B = { a: 123 } // never

Now, defining GetType becomes a straightforward check for never.

type GetType<T> =
    KeysWithFunctions<T> extends never
        ? 'group'
        : 'slice'

This logic should give you the desired outcome:

type A = GetType<{ data: {}, upload(): void }>
//   slice

type B = GetType<{ data: {} }>
//   group

type C = GetType<{ data: {}, nested: { data2: 20, fn: () => void } }>
//   group

See Playground

Answer №2

Mark Hana pointed out in a comment that,

In TypeScript, the type {} does not signify "any object". Instead, it means "any value where attempting to access a property won't throw an error"

This implies:

type EqualsTrue = ((() => void) | {}) extends {} ? true : false
// or more precisely
type EqualsTrue2 = Function extends {} ? true : false

It may seem unusual, but this is a hurdle we face. However, the reverse scenario is not valid! We can take advantage of this fact:

type EqualsFalse = {} extends Function ? true : false

Let's create a utility type, essentially a filter function, that removes Function types from the input union.

type CheckExtends<T, U> = T extends U ? T : never
// usage:
type TypeArgs = {} | (() => void) | number | (() => boolean)
type TypeVal = CheckExtends<TypeArgs, Function>
//   ^? type TypeVal = (() => void) | (() => boolean)

Now we can apply the S[keyof S] part into this filter for the value union, which can result in all the functions types or never. With this insight, the solution becomes clear:

type GetType<S> = CheckExtends<S[keyof S], Function> extends never ? "group" : "slice"

Playground

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

What is the best way to import a data type from another file into a `.d.ts` file without converting it into a module?

Recently encountered a peculiar scenario involving d.ts files and namespaces. I have some d.ts files where I define and merge a namespace called PROJECT. Take a look at how it's declared and automatically merged (across multiple files) below: file1 ...

What is the best way to categorize a collection of objects within a string based on their distinct properties?

I am working with an array of hundreds of objects in JavaScript, each object follows this structure : object1 = { objectClass : Car, parentClass : Vehicle, name : BMW } object2 = { objectClass : Bicycle, parentClass : Vehicle, name : Giant } object3 = { ob ...

Typed NextJs navigation to a specific route

<Link href="/about"> <a>About Us</a> </Link> Is there a way to ensure type safety with NextJs links? Currently, it is challenging to restructure the Link component as it is just a string. I stumbled upon this repos ...

Establish a many-to-many relationship in Prisma where one of the fields is sourced from a separate table

I'm currently working with a Prisma schema that includes products, orders, and a many-to-many relationship between them. My goal is to store the product price in the relation table so that I can capture the price of the product at the time of sale, re ...

"Continue to shine until the rendering function displays its source code

I've encountered a strange issue where I'm using lit until to wait for a promise to return the template, but instead of the desired output, the until's function code is being rendered. For example: render() { return html` <div c ...

Is it feasible to have two interfaces in Typescript that reference each other?

I am facing an issue with two interfaces, UserProfile and Interest. Here is the code for both interfaces: export default interface UserProfile { userProfileId: string, rep: number, pfpUrl: string, bio: string, experience: "beginner" | " ...

I am attempting to update the URL of an iframe dynamically, but I am encountering an issue: the Error message stating that an Unsafe value is being

Currently, I am attempting to dynamically alter the src of an iframe using Angular 2. Here is what I have tried: HTML <iframe class="controls" width="580" height="780" src="{{controllerSrc}}" frameborder="0" allowfullscreen></iframe> COMPONE ...

Steps for building a custom component using MUI as a foundation

My current approach: import React from "react"; import {useHistory} from "react-router-dom"; import {Button, ButtonProps} from "@mui/material"; type Props = { label?: string } & ButtonProps; export const NavBackButton = ...

One way to display a table is by populating it with data from an API. If the table does

Within my Angular 6 application, there exists a table that displays data fetched from a web api. Additionally, I have incorporated some ngIf containers. One of these containers is programmed to exhibit a message in case the web api data turns out to be emp ...

Exploring the TypeScript compiler API to read and make updates to objects is an interesting

I'm delving into the world of the typescript compiler API and it seems like there's something I am overlooking. I am trying to find a way to update a specific object in a .ts file using the compiler API. Current file - some-constant.ts export co ...

Leveraging Global Variables and Functions with Webpack and TypeScript

I have been utilizing Webpack in conjunction with TypeScript, HTML, and SCSS to develop a project. My goal is to create a single page application that incorporates a router located within the root folder of the project /framework/, with all my source code ...

Safe way to implement map and spread operator in your codebase

Is there a workaround for this issue? I am working with an interface, IFoo, and an array of data IFoo[]. My goal is to map this data and modify a single property. It should look something like this const mapper = (foos: IFoo[]): IFoo[] => { return foo ...

What is the best way to fetch data before a component is rendered on the screen?

I am facing an issue with fetching data from a local server in Node. When I try to render the component, the array 'users' from the state appears to be empty, resulting in no users being displayed on the screen as intended. What's strange is ...

Modify the color of Material UI's Select Component's IconComponent

Currently in my project, I am utilizing MUI's Select Component with the LanguageIcon as the designated IconComponent. My goal is to change the color of this icon from black (default) to white, but I have been unsuccessful in my attempts. I attempte ...

Importing CSS properties from MUI v5 - A comprehensive guide

I'm working with several components that receive styles as props, such as: import { CSSProperties } from '@material-ui/styles/withStyles' // using mui v4 import because unsure how to import from v5 paths import { styled } from '@mui/mat ...

What might be causing my action function to be triggered during the rendering process?

While working on creating a basic card view in material UI, I encountered an issue where the functions for adding and deleting items seem to be triggered multiple times upon rendering. I am aware that a common reason for this could be using action={myFunc ...

Strategies for obtaining the return type of a map and only including the type S

How can I retrieve the return type of map and only display S? const initialState = { x: 1, y: 2 } type InitialStateType = typeof initialState type MapGetter<S, R, R1> = { map: (state: S) => R mapB?: (state: S) => R1 // more ... } ...

Adding TH into a TABLE in Angular 2 by verifying TD elements

I am seeking a way to automatically generate thead and th, using td in the template : <Datatable> <tr #lineSelected *ngFor="let subscription of results"> <td nameColumn="Nom">{{subscription.name}}</td> <td n ...

Issue in React Native and Firestore: The 'auth' property is not recognized in the specified type or an error object of unknown type

I am currently using Typescript in conjunction with Firebase and React Native. While working on the signup screen, I included Firebase like so: import * as firebase from 'firebase/app'; import 'firebase/auth'; In my onSignUp function, ...

Challenges with overwriting TailwindCSS classes within a React component library package

I just released my very first React component on NPM. It's a unique slider with fractions that can be easily dragged. Check it out here: Fractional Range Slider NPM This slider was created using TailwindCSS. During bundling, all the necessary classe ...