Tips for refining the types produced by a personal component that incorporates a third-party function/types

I have created a wrapper component for the Shopify Resource Picker.

import { useAppBridge } from '@shopify/app-bridge-react';
import { Button, type ButtonProps } from '@shopify/polaris';
import { useCallback } from 'react';

export type ResourcePickerSelectPayload = NonNullable<
  Awaited<ReturnType<typeof shopify.resourcePicker>>
>;

interface ProductResourcePickerProps {
  onSelect: (resources: ResourcePickerSelectPayload) => void;
  options?: Parameters<typeof shopify.resourcePicker>[0];
  buttonLabel?: string;
  buttonProps?: Omit<ButtonProps, 'onClick'>;
}

export function ResourcePicker({
  onSelect,
  options = { type: 'product' },
  buttonLabel = 'Select products',
  buttonProps = {},
}: ProductResourcePickerProps) {
  const shopify = useAppBridge();

  const handleOpenPicker = useCallback(async () => {
    const selected = await shopify.resourcePicker(options);

    if (selected) {
      onSelect(selected);
    }
  }, [onSelect, options, shopify]);

  return (
    <Button onClick={handleOpenPicker} {...buttonProps}>
      {buttonLabel}
    </Button>
  );
}

The result of shopify.ResourcePicker is defined as follows:

type ResourcePickerApi = (options: ResourcePickerOptions) => Promise<SelectPayload<ResourcePickerOptions['type']> | undefined>;

This leads to my ResourcePickerSelectPayload being:

type ResourcePickerSelectPayload = ResourceSelection<"product" | "variant" | "collection">[] & {
    selection: ResourceSelection<"product" | "variant" | "collection">[];
}

I am having difficulty figuring out how to pass the value from options.type (product, variant, or collection) so that my component can return the specific type. For example, if options.type === 'product', then selected should be of type ResourceSelection<'product'>. Since the ResourceSelection type is not exported, I am unsure how to access it other than what I currently have. I believe the problem lies in the fact that my ResourcePickerSelectPayload is not generic, while ResourceSelection is.

I attempted to pass the value of options.type into my function and incorporate conditional returns, but unfortunately, it did not work as expected.

Answer №1

Utilizing generics can greatly enhance the functionality in this scenario. It essentially permits you to dynamically deduce and uphold types based on the given input.

By leveraging generics, we could revamp the code to appear as follows;

interface ProductResourcePickerProps<
  T extends "product" | "variant" | "collection"
> {
  onSelect: (resources: ResourceSelection<T>[]) => void;
  options?: { type: T } & Omit<ResourcePickerOptions, "type">;
  buttonLabel?: string;
  buttonProps?: Omit<ButtonProps, "onClick">;
}

export function ResourcePicker<T extends "product" | "variant" | "collection">({
  onSelect,
  options = { type: "product" } as { type: T },
  buttonLabel = "Select resources",
  buttonProps = {},
}: ProductResourcePickerProps<T>) {
  const shopify = useAppBridge();

  const handleOpenPicker = useCallback(async () => {
    const selected = await shopify.resourcePicker(options);

    if (selected?.selection) {
      onSelect(selected.selection);
    }
  }, [onSelect, options, shopify]);

  return (
    <Button onClick={handleOpenPicker} {...buttonProps}>
      {buttonLabel}
    </Button>
  );
}

Hence, upon invoking ResourcePicker, onSelect is already aware of the incoming data. An example of its usage would be;

<ResourcePicker
  onSelect={(selected) => {
    // TypeScript indicates ResourceSelection<'product'>[] as the existing type
    console.log(selected);
  }}
  options={{ type: 'product' }}
  buttonLabel="Select products"
/>

In my review of the resource picker documentation, it appears that there may be further enhancements needed within this wrapper. However, focusing on the provided code snippet, the incorporation of generics effectively addresses the issue at hand.

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

Using the recommended Prettier plugin for TypeScript in ESLint is causing issues with the ability to use the override keyword

Software Versions: <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6a0f190603041e2a5d445958445a">[email protected]</a> <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7000021504041915023 ...

Switching buttons with AngularJS

I am currently working on a Github search app using the Github API in Angular. My goal is to make it so that when the user clicks the "Add to Favorite" button, the button disappears and the "Remove Favorite" button is displayed instead. I attempted to achi ...

Display customizable template according to variable

The answer provided for the issue regarding dynamic template generation based on value instead of variable in this thread was really helpful. However, I'm facing challenges in getting it to work. Here's a simplified example: export class A { } ...

The index type 'X' is not allowed for use in this scenario

I encountered an issue in my TypeScript code: error message: 'Type 'TransitionStyles' cannot be used as an index type.' I'm wondering if there's a way to modify my interface so that it can be used as an index type as well: ...

The argument represented by 'T' does not align with the parameter represented by 'number' and therefore cannot be assigned

I am confused as to why, in my situation, <T> is considered a number but cannot be assigned to a parameter of type number. Changing the type of n to either number or any resolves the issue. Error: https://i.sstatic.net/h1GE9.png Code: const dropF ...

Callback for dispatching a union type

I am currently in the process of developing a versatile function that will be used for creating callback actions. However, I am facing some uncertainty on how to handle union types in this particular scenario. The function is designed to take a type as inp ...

Resetting a Subject entity

Is there a way to reset an Observable object? I'm not sure if "reinitialize" is the right term, but essentially what I want is to update the data without creating a new Observable object or new subscriptions. I want existing subscriptions to seamless ...

Different ways to handle dialogs in React with TypeScript

I'm currently working on developing a modal component in React TypeScript and I'm facing some issues in determining the correct type for a reference of an HTML dialog element. import { useRef } from 'react' const MyModal: React.FC = () ...

Struggling with organizing nested query data in Firebase/Firestore using React and Typescript

I've been working on rendering data from sections and their corresponding lectures saved within those sections in Firestore. My approach involves querying the section collection first, and then fetching the lecture data using the section ID. While I ...

Establish a default value for a TypeScript generic Type: distinguishing between 'unknown' and 'any'

An interface has been created with a parameter that takes a generic type input named Data export interface MyStructure<Data> { id: string; data: Data; } The goal is to allow the Data type to be optional in order to support scenarios like: functi ...

TypeScript generic types allow you to create reusable components that

function genericIdentity<T>(arg: T): T { return arg; } let myGenericIdentity: <U>(arg: U) => U = genericIdentity; I see that the 'genericIdentity' function is accepting an argument of a generic type. However, I am unsure about ...

NG0303: Unable to establish a connection with 'ngbTooltip' as it is not recognized as a valid property of 'button'

ERROR: 'NG0303: Can't bind to 'ngbTooltip' since it isn't a known property of 'button'.' Encountering this issue in my Angular 12 project when running local tests, the ngbTooltip error is present in all .spec files. ...

Ways to incorporate horizontal scrolling in mat autocomplete

I have an Angular app with auto complete functionality. I am trying to add horizontal scroll to the mat-option by applying CSS styles, but it's not working as expected. .cdk-overlay-pane { overflow-x: auto; } I also tried following the instruc ...

Dealing with the Angular 7 ExpressionChangedAfterItHasBeenCheckedError in combination with NgsScrollReveal

Utilizing ngScrollReveal triggers a re-render with every scroll event. I am invoking a function through the HTML in this manner: <component [alternate]="toggleAlternate()"> The code for toggleAlternate() is as follows: toggleAlternate() { this.a ...

To handle async actions in Typescript with React and Redux, ensure that all actions passed to axios are plain objects. If you need to perform

Looking for assistance with Typescript, React, and Redux. export function fetchAllMeals (subject: string){ axios .get(`https://www.themealdb.com/api/json/v1/1/search.php?s=${subject}`) .then((response: any) => { console.log(response.data) ...

Why does "excess property checking" seem pleased when I utilize a key from set A or set B, even though "keyof (A|B)" is consistently "never"?

I am diving deeper into Typescript types and encountering some puzzling behavior: interface Person { name: string; } interface Lifespan { birth: number; death?: number; } let k: keyof (Person | Lifespan); //k is never let test1: Person | Life ...

Angular allows you to bind a Checkbox input type to be checked by default

Working on an Angular 5 application with checkboxes. Data is retrieved from the database and mapped to a JavaScript object. Having trouble configuring the 'Selected' value. Currently, all checkboxes are checked in the result. In the code below, ...

Tips for utilizing the Fluent UI Northstar Color Palette

When working with Fluent UI Northstar, one helpful feature is the color palette. While the documentation provides a list of color names and gradients that can be found here, it can be confusing on how to actually utilize these values (such as 100, 200, e ...

Tips for building a React component map with accurate typing

I am currently working on creating a React component map using TypeScript. This map returns the corresponding component based on the key, where both Item1 and Item2 are React components. const componentMap: {[key: string]: JSX.Element} = { "Item1&quo ...

What is the best way to combine individual function declarations in TypeScript?

In my project, I am currently developing a TypeScript version of the async library, specifically focusing on creating an *-as-promised version. To achieve this, I am utilizing the types provided by @types/async. One issue I have encountered is that in the ...