What is the reason for a high-order generic function to eliminate falsy types from the argument list of the returned function?

Consider this example of a unique Decorator Factory Builder - a builder that constructs Decorator Factories to define metadata for forms:

interface FormFieldOptions {
  name?: string;
  label?: string;
  format?: string;
}

type FormProperties = Record<string | symbol, FormFieldOptions>;

function FormFieldPropertyDecorator<K extends keyof FormFieldOptions>(
  option: K,
  defaultValue?: FormFieldOptions[K] | 'propertyKey',
) {
  return function FormFieldProperty(value: FormFieldOptions[K] | null) {
    return function (target: unknown, propertyKey: string | symbol) {
      // ... Reflection magic goes here ...
    };
  };
}

The type

value: FormFieldOptions[K] | null
is converted to value: FormFieldOptions[K] when using the Decorator Factories:

// const FormField: (value: string) => (target: unknown, propertyKey: string | symbol) => void
export const FormField = FormFieldPropertyDecorator('name', 'propertyKey');
// const Label: (value: string) => (target: unknown, propertyKey: string | symbol) => void
export const Label = FormFieldPropertyDecorator('label');
// const Format: (value: string) => (target: unknown, propertyKey: string | symbol) => void
export const Format = FormFieldPropertyDecorator('format');

A similar transformation occurs with the type

FormFieldProperty(value: FormFieldOptions[K] | undefined)

However, the optional modifier ? remains in the returned function types:
Using

return function FormFieldProperty(value?: FormFieldOptions[K] | null) {

// const FormField: (value?: string) => (target: unknown, propertyKey: string | symbol) => void
export const FormField = FormFieldPropertyDecorator('name', 'propertyKey');
// const Label: (value?: string) => (target: unknown, propertyKey: string | symbol) => void
export const Label = FormFieldPropertyDecorator('label');
// const Format: (value?: string) => (target: unknown, propertyKey: string | symbol) => void
export const Format = FormFieldPropertyDecorator('format');

Why do null or undefined types disappear, while the optional modifier remains unchanged?

PS: The main goal here is to make value mandatory if defaultValue is not set, but optional otherwise

Answer №1

It's not entirely clear why you assume that the type is coerced; does your IDE indicate that? When using the initial definition, you must invoke the returned function (FormFieldProperty) as specified by its signature, meaning either with something of type FormFieldOptions[K] or with the value null; both of these options are acceptable:

export const Label = FormFieldPropertyDecorator('label');
Label("something");
Label(null);

If you wish to achieve the described behavior, you could utilize function overloading:

type InternalDecorator = (target: unknown, propertyKey: (string | symbol)) => void;

function FormFieldPropertyDecorator<K extends keyof FormFieldOptions>(
    option: K,
): (value: string) => InternalDecorator;
function FormFieldPropertyDecorator<K extends keyof FormFieldOptions>(
    option: K,
    defaultValue: FormFieldOptions[K] | 'propertyKey',
): (value?: string) => InternalDecorator;

function FormFieldPropertyDecorator<K extends keyof FormFieldOptions>(
    option: K,
    defaultValue?: FormFieldOptions[K] | 'propertyKey',
) {
    return function FormFieldProperty(value: FormFieldOptions[K]) {
        return function (target: unknown, propertyKey: string | symbol) {
            // ... Some reflect-metadata magic ...
        };
    };
}

export const Label1 = FormFieldPropertyDecorator('label');
Label1("something"); // OK
Label1();            // not OK

export const Label2 = FormFieldPropertyDecorator('label', "something");
Label2("something"); // OK
Label2();            // also OK

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

Only 1 argument was provided instead of the expected 2 when attempting to update Firebase through Ionic

I encountered an issue while trying to update Firebase from Ionic. Below is the function I used to update the data. Below is the TypeScript code: updateLaporan() { this.id = this.fire.auth.currentUser.uid; this.db.list('/laporan/'+thi ...

Incorporating type declarations for a basic function that returns a wrapper function for other functions

In my vanilla JS code, I have a sophisticated function that is exported and I am attempting to create a .d.ts file for it. Unfortunately, my previous attempts at writing the .d.ts file have not been successful in passing the types from one stage of the fu ...

Exploring the ambiguity of explicit types with type generics

I am facing a type error issue with a Higher-Order Component (HOC): Error: Type 'number[]' is not assignable to type 'string'. Here is how I set up my HOC: const ComponentPage = renderPage(Component); However, I encounter the error ...

Unable to grasp the mistake

My code contains a function: buy() { return new Promise((resolve, reject) => { this.http.request('http://192.168.1.131:8888/generatetoken.php') .subscribe(res => { resolve(res.text()); }); }).then((key) => ...

Organizing Firebase functions: Managing multiple functions and dependencies

Objective I aim to gain a deeper understanding of how the firebase CLI manages the deployment process for multiple firebase functions, each stored in different files, and how it handles dependencies that are specific to certain functions. Situation If I ...

Error message shows explicit Typescript type instead of using generic type name

I am looking to use a more explicit name such as userId instead of the type number in my error message for error types. export const primaryKey: PrimaryKey = `CONSUMPTION#123a4`; // The error 'Type ""CONSUMPTION#123a4"" is not assignable to ...

What methods should I employ to effectively test a custom icon function?

I've written a function that creates a Leaflet icon with specified properties: createIcon( url, retinaUrl: string = null, height: number = 20, width: number = 20 ): Icon { const icon: Icon = L.icon({ iconUrl: url, ico ...

Does this Spread Operator Usage Check Out?

Upon reviewing Angular's API documentation, I came across the declaration for the clone() method in HttpRequest as follows: clone(update: { headers?: HttpHeaders; reportProgress?: boolean; params?: HttpParams; responseType?: "arraybuffer" ...

Can a custom subscribe() method be implemented for Angular 2's http service?

Trying my hand at angular2, I discovered the necessity of using the subscribe() method to fetch the results from a get or post method: this.http.post(path, item).subscribe( (response: Response)=> {console.log(response)}, (error: any)=>{console.l ...

What is the best way to prevent updating the state before the selection of the end date in a date range using react-datepicker?

Managing user input values in my application to render a chart has been a bit tricky. Users select a start date, an end date, and another parameter to generate the chart. The issue arises when users need to edit the dates using react-datepicker. When the s ...

How is it possible to utilize type assertions with literals like `false`?

When working in TypeScript, I came across an interesting observation when compiling the following code: const x = true as false; Surprisingly, this direct assertion is valid, creating a constant x with the value true and type false. This differs from the ...

Angular CLI - exploring the depths of parent-child component communication

My issue revolves around accessing the 'edit' method of a child component using @ViewChild, but for some reason it's not functioning as expected. Where could I possibly be going wrong? Here are the console logs: https://i.sstatic.net/wvpVN ...

Make sure to name your Typescript component selector correctly, as it should not

As I work on my Angular project, I encountered a situation where one component needed to be referenced in the HTML of another component. To make this connection, I used kebab case for the selector like so: @Component({ selector: 'swiftlog-navbar&ap ...

What is the reasoning behind ethers.js choosing to have the return value of a function be an array that contains the value, rather than just the value itself

An issue arose with the test case below: it('should access MAX_COUNT', async () => { const maxCount = await myContract.functions.MAX_COUNT(); expect(maxCount).to.equal(64); }); The test failed with this error message: ...

Navigating an array using ngFor and encountering an error message saying, "Identifier not specified"

While using ngFor to iterate through an array, I encountered the following error message: "Identifier 'expenseitem' is not defined. The component declaration, template variable declarations, and element references do not contain such a memb ...

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 ...

Webpack 2.7.0 throws an error: "Unexpected parameter: theme"

At the moment, I am on webpack 1.16.0 and using --theme as an argument to set the output path and plugin paths. The command appears as: rimraf dist && webpack --bail --progress --profile --theme=<name of theme> However, as I try to upgrade ...

Ways to redirect to a different page following a successful execution of a mutation in React-query

I am facing an issue where a memory leak warning appears when I redirect to another page after a mutation. Despite trying various methods, I have not been able to find a solution. The specific warning message is: Warning: Can't perform a React state ...

index signature in TypeScript is an optional feature

Is it possible to create a type with optional namespaces in TypeScript? export interface NodesState { attr1: number; attr2: number; attr3: number; } The goal is to allow users to namespace the type like this: { namespace1: { attr1: 100, ...

The element at index '0' is not defined for the data type 'number | [number, number]'

In my current project, I have a component named ComponentA which has a defined interface. Here is the snippet of the interface: interface A1 { isSingle: true; value: number; } interface A2 { isSingle: false; value: [number, number]; } exp ...