Determine the return type of a function based on the parameter type that is inferred

I am seeking to define a simple function that checks a condition and returns either a value or a default. In most cases, the default is constant, so it would be beneficial to return a union of expected and default value types to properly narrow it down at the call site.

First, let's look at the function and its expected use cases.

const func = (
  value: unknown, defaultValue?: string | null | undefined
): string | null | undefined => {
  if (typeof value === 'string') {
    return value;
  }

  return defaultValue;
};

const case1: string = func('', 's');
const case2: string | null = func('', null);
const case3: string | undefined = func('', undefined);
const case4: string | undefined = func('');
const x: string | null | undefined = '';
const case5: string | null | undefined = func('', x);

The first argument type always remains the same and does not affect discrimination, included here for illustration purposes. The output type always contains a string. Discrimination should only impact whether the output type contains null and undefined based on the default parameter type at the call site.

With the specified function type, the compiler raises errors for cases 1-4.

Now, I have tried various approaches to defining a discriminating type.

interface CustomType {
  (value: unknown, defaultValue: string): string;
  (value: unknown, defaultValue: null): string | null;
  (value: unknown, defaultValue?: undefined): string | undefined;
}

type Type1 = ((value: unknown, defaultValue: string) => string) | 
  ((value: unknown, defaultValue: null) => string | null) | 
  ((value: unknown, defaultValue?: undefined) => string | undefined) |
  ((value: unknown, defaultValue: string | null | undefined) => string | null | undefined)

type Type2 = <U extends string | null | undefined> (
  value: unknown, defaultValue?: U
) => string | U;

The interface "CustomType" with overloads covers all use cases, but the function "func" cannot be assigned to it even when the last overload exactly matches "func"'s type. Overloads tend to be verbose in nature.

"func" can be assigned to Type1, however, it does not function as overloads.

Type2 offers a concise solution that satisfies all use cases, yet "func" still cannot be assigned to it. Moreover, Type2 allows incorrect use cases like

const incorrect: number = func('', <any>1);
, due to the forgiving nature of extends.

Is there a way to create a type that describes the same overloads as the aforementioned interface? Can we declare the function in a manner that makes it assignable to both the interface and the type?

Answer №1

I believe that your T2 is almost accurate enough for practical use, however, please note that f('') will return type string | null | undefined. If you wish to make it more precise, you could consider the following approach:

const f = <T extends [(string | null)?]>(
    value: unknown, ...[defaultValue]: T
): string | T[0] => {
    if (typeof value === 'string') {
        return value;
    }
    return defaultValue;
};

This method utilizes rest parameters and tuple types to ensure that when defaultValue is excluded, the generic type parameter defaults to undefined rather than string | null. It's intriguing how the inference works in scenarios like these.

All your test cases validate the function:

const s1 = f('', 's'); // string
const s2 = f('', null); // string | null
const s3 = f('', undefined); // string | undefined 
const s4 = f(''); // string | undefined 
const x = ["", null, void 0][Math.floor(Math.random() * 3)]; // string | null | undefined
const s5 = f('', x); // string | null | undefined

Perhaps replacing T2 with something similar is a better idea:

// typeof f
type T3 = <T extends [(string | null | undefined)?]>(
    value: unknown, ...[defaultValue]: T
) => string | T[0];

You'll observe that this aligns well with the usage of I:

const i: I = f; // okay

In regard to using T2 (or T3) resulting in:

const incorrect = f('', <any>1); // any 🤷‍

I would caution against utilizing values of type any, as it can introduce unexpected issues into the type system. While detecting occurrences of any is possible, it may complicate the function typing excessively:

type IfAny<T, Y, N = T> = 0 extends (1 & T) ? Y : N;
const g = <T extends [(string | null)?]>(
    value: unknown, ...[defaultValue]: T
): string | IfAny<T[0], null | undefined> => {
    if (typeof value === 'string') {
        return value;
    }
    return defaultValue as any;
};

const gs1 = g('', 's'); // string
const gs2 = g('', null); // string | null
const gs3 = g('', undefined); // string | undefined 
const gs4 = g(''); // string | undefined 
const gs5 = g('', x); // string | null | undefined
const betterMaybe = g('', <any>1); // string | null | undefined

A bit complicated, but it might work.


Hope this explanation aids you in solving the issue. Good luck!

Link to original code

Answer №2

Here is an example of how to implement discriminated types in order to work with the function f:

interface IDiscriminate {
    (value: string, defaultValue?: unknown): string;
    <T>(value: unknown, defaultValue?: T): T;
}

const f: IDiscriminate = <T>(value: unknown, defaultValue?: T) => {
    if (typeof value === 'string') {
        return value;
    }
    return defaultValue;
};

const s1: string = f('', 's');
const s2: string = f('', null);
const s3: string = f('', undefined);
const s4: string = f('');
const x: string = 'x';
const s5: string = f('', x);
const s6: string = f(1, x);
const s7: null = f(1, null);
const s8: undefined = f(true);
const s9: number = f(1, 1);
//const incorrect: number = f('', <any>1); // Fails to compile

Testing

console.log(`s1 === '': ${s1 === ''}`); // true
console.log(`s2 === '': ${s2 === ''}`); // true
console.log(`s3 === '': ${s3 === ''}`); // true
console.log(`s4 === '': ${s4 === ''}`); // true
console.log(`s5 === '': ${s5 === ''}`); // true
console.log(`s6 === 'x': ${s6 === 'x'}`); // true
console.log(`s7 === null: ${s7 === null}`); // true
console.log(`s8 === undefined: ${s8 === undefined}`); // true
console.log(`s9 === 1: ${s9 === 1}`); // true

You can view the results of running this test on the 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

Is there a way to help my KafkaJS consumer stay patient while waiting for a broker to become available?

My KafkaJS consumer setup looks like this: // Create the kafka client const kafka = new Kafka({ clientId, brokers, }); // Create the consumer const consumer = this.kafka.consumer({ groupId, heartbeatInterval: 3000, sessionTimeout: 30000, }); // ...

Error encountered in jquery.d.ts file: Unresolved syntax issue

I am currently in the process of transitioning my jQuery project into a Typescript project. I encountered an issue when attempting to include the jQuery typings from the DefinitelyTyped Github by using the following method: ///<reference path="../../ty ...

Tips for optimizing HttpRequests within nested for-loops that utilize subscribe()?

Our REST-API is designed to be frontend agnostic, meaning it always sends the IRI to nested resources. This results in multiple HTTP calls needed to retrieve data; first for the parent resource, then its child resources and so on. Each Country has linked E ...

Troubleshooting mistakes in Typescript import syntax and beyond

During the development of this project in react and typescript using create-react-app, I encountered no issues. Now, my aim is to publish one of the components on npm. I have come to understand that I need to build this component separately from an existi ...

Make an indirect mention of a distant JavaScript web address

Our company is looking to incorporate Rollup with Angular 4/Typescript and NPM, and we have a specific set of requirements: Various teams develop JS libraries that need to be centralized, similar to a CDN These libraries are hosted at remote URLs and sho ...

Typescript - type assertion does not throw an error for an invalid value

When assigning a boolean for the key, I have to use a type assertion for the variable 'day' to avoid any errors. I don't simply do const day: Day = 2 because the value I receive is a string (dynamic value), and a type assertion is necessary ...

Having issues with an Angular reactive form that includes a custom form-level validator and the 'blur' updateOn option?

Having issues combining the following: angular reactive form custom validator at form level (cross-field validator) usage of the 'updateOn' option set to 'blur' A demonstration of the problem can be found in this simple stackblitz: h ...

Is it possible for a TypeScript function to be used as a React prop even if it conflicts with the function signature's in

Why does the TypeScript type checker allow a prop with a function parameter that does not strictly match the definition? For example, I have a function called callbackImpl = (str: string): number, which is passed as a React prop parameter defined as callb ...

What is the proper method for typing unidentified exports that are to be used in TypeScript through named imports?

Currently, I am developing an NPM package that takes the process.env, transforms it, and then exports the transformed environment for easier usage. The module is structured like this: const transformedEnv = transform(process.env) module.exports = transf ...

Is there a way to send both a file and JSON data in a single HTTP request?

Once I developed a small application using NestJs where I implemented a BFF (Backend for Frontend) service. Within this service, I tried to execute a POST request to create a new user while also including the user's avatar in the same request. Here is ...

utilize Angular's interface-calling capabilities

In the backend, I have an interface structured like this: export interface DailyGoal extends Base { date: Date; percentage: number; } Now, I am attempting to reference that in my component.ts file import { DailyGoal } from '../../interfaces' ...

Is there a way to bypass the "Error: Another application is currently displaying over Chrome" message using Javascript or Typescript?

Can the "Another app is displaying over chrome error" be bypassed using JavaScript or TypeScript? Error Message: https://i.stack.imgur.com/iSEuk.png ...

delayed updating of property not visible in angular 10 immediately

I needed to hide a div based on a condition, so I decided to use the hidden property like below: <div [hidden]="isControlDisplayed()?false:true"> The isControlDisplayed() method determines whether to show or hide the div based on the value ...

Is it possible to utilize [key:string]: any in order to eliminate the requirement for an additional function when working

Currently, I am utilizing recompose within my React project. Specifically, I am leveraging its compose function which is defined as: function compose<TInner, TOutter>( ...functions: Function[] ): ComponentEnhancer<TInner, TOutter>; interf ...

Refine the observable data

Trying to filter a list of items from my Firebase database based on location.liked === true has been a challenge for me. I've attempted using the traditional filter array method but have not had success. Can anyone suggest an alternative method to acc ...

Using Typescript to define Vuex store types

Attempting to create a TypeScript-friendly Vuex store has been quite the challenge. Following instructions outlined here, I've encountered an issue where accessing this.$store from a component results in a type of Store<any>. I'm strugglin ...

Debug errors occur when binding to computed getters in Angular 2

Currently, I am integrating Angular 2 with lodash in my project. Within my model, I have Relations and a specific getter implemented as follows: get relationsPerType() { return _(this.Relations) .groupBy(p => p.Type) .toPairs() ...

What is the best way to implement ES2023 functionalities in TypeScript?

I'm facing an issue while trying to utilize the ES2023 toReversed() method in TypeScript within my Next.js project. When building, I encounter the following error: Type error: Property 'toReversed' does not exist on type 'Job[]'. ...

Is there a way to imitate a tab click on Angular Material's md-tab

How can I programmatically trigger a click on an md-tab element? For instance, if there is a button on my webpage that is unrelated to the md-tab element, but I want it to switch the md-tab group to a specific tab when clicked, what would be the best app ...

Building a versatile component library for Next.js using TypeScript and Tailwind CSS: Step-by-step guide

Lately, I've been utilizing Next.js and crafting components such as buttons, inputs, and cards with Tailwind CSS for my various projects. However, the repetitive task of rewriting these components from scratch for each new project has become quite tir ...