Exploring Typescript's 'Conditional' Typing Features

My function has a straightforward task in theory, but its type description is lacking, leading to the need for type assertions whenever I work with the data.

The function:

const fetch_and_encode = <T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<T | E> | T | E,
    encode?: Encoder<T, E, C>,
    context?: C
}): E => {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = (source as Fetcher<T | E>)();
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded as T, context);
    } else {
        return decoded as E;
    }
};

The defined types:

export type Encoded = number | string | ArrayBuffer // | Array<Encoded> | Map<string, Encoded>
export type Fetcher<T> = () => T;
export type Encoder<T, E extends Encoded, C> = (decoded: T, context?: C) => E;

This function essentially deals with two variables, source and encode, each with two possible types, resulting in four potential states. The variable source can be either a piece of data or a function that retrieves data. On the other hand, encode could be undefined or a function that transforms the output of source. Ultimately, the combination should produce a value of a relatively simple type, Encoded.

I've made several attempts to refine the type definition without success, constantly requiring type assertions. These efforts were more about deepening my knowledge of the type system rather than just cleaning up definitions. It seems like there should be a way to specify the types tightly enough to avoid the need for these assertions, and I'd like to figure out how.

One approach I tried using unions didn't actually enhance the type definition:

const fetch_and_encode = <T, E extends Encoded, C>( {source, encode, context}: {
    source: Fetcher<T>;
    encode: Encoder<T, E, C>;
    context?: C;
} | {
    source: Exclude<T, Function>; 
    encode: Encoder<T, E, C>;
    context?: C;
} | {
    source: Fetcher<E>;
    encode: undefined;
    context?: any;
} | {
    source: E;
    encode: undefined;
    context?: any;
}): E => {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = (source as Fetcher<T | E>)();
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded as T, context);
    } else {
        return decoded as E;
    }
};

Another attempt using conditional types also hit a dead end:

const fetch_and_encode = <T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<T | E> | T | E,
    encode: Encoder<T, E, C> | undefined,
    context: C | undefined
} extends { source: infer S, encode: infer N, context?: C }
    ? S extends Function 
        ? S extends Fetcher<T | E>
            ? N extends undefined
                ? { source: Fetcher<E>; encode: undefined; context?: any; }
                : { source: Fetcher<T>; encode: Encoder<T, E, C>; context?: C; }
            : never
        : N extends undefined
            ? { source: E; encode: undefined; context?: any; }
            : { source: T; encode: Encoder<T, E, C>; context?: C; }
    : never
): E => {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = (source as Fetcher<T | E>)();
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded as T, context);
    } else {
        return decoded as E;
    }
};

I'm at a loss on where to go from here.

Following the suggestion by Ingo Bürk, I experimented with overloads which resolved the initial issues but introduced a new problem that puzzles me:

function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    source: E;
    encode: undefined;
    context?: any;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<E>;
    encode: undefined;
    context?: any;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<T>;
    encode: Encoder<T, E, C>;
    context?: C;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    source: Exclude<T, Function>;
    encode: Encoder<T, E, C>;
    context?: C;
}): E {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = source();
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded, context);
    } else {
        return decoded;
    }
}

If I include my current generic definition as the default, the error mentioned above disappears, but then I'm back to needing type assertions once again.

Answer №1

Here is a method using overloads to achieve the desired functionality. The function body itself remains untyped due to difficulties in implementation, but the function calls are correctly typed.

function isFetcher<T>(obj: T | Fetcher<T>): obj is Fetcher<T> {
  return typeof obj === "function";
}

function fetchAndEncode<A extends Encoded>(source: A | Fetcher<A>): A;
function fetchAndEncode<A, B extends Encoded, C>(source: A | Fetcher<A>, encode: Encoder<A, B, C>, context?: C): B;
function fetchAndEncode(source: any, encode?: any, context?: any) {
  const datum = isFetcher(source) ? source() : source;
  return encode ? encode(datum, context) : datum;
}

The following type tests are successfully passed:

let numericValue: number;

fetchAndEncode(numericValue); // returns number
fetchAndEncode(true); // returns error
fetchAndEncode(numericValue, val => `Hello ${val}`); // returns string
fetchAndEncode(() => numericValue); // returns number
fetchAndEncode(() => true); // returns error
fetchAndEncode(() => numericValue, val => `Hello ${val}`); // returns string

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

Unable to access pathways from a separate source

In my app.component.ts file, I have two router outlets defined, one with the name 'popup': @Component({ selector: 'app-main', template: `<router-outlet></router-outlet> <router-outlet name="popup" ...

Preserving quotation marks when utilizing JSON parsing

Whenever I try to search for an answer to this question, I am unable to find any relevant results. So please excuse me if this has been asked before in a different way. I want to preserve all quotation marks in my JSON when converting from a string. In m ...

I am having trouble installing the TypeScript Plugin for next.js on my VSCode

Attempting to kick off a simple project on Next.js with TypeScript and version 13.4 of Next proved to be a challenge for me. On both occasions, the pop-up in the bottom-right corner did not appear, and the command ctrl+shift+p did not yield any "TypeScript ...

What is the method for extracting date of birth data from .NET to Angular?

Struggling to fetch the date of birth from the database where it has been stored. After searching through multiple resources online, I am still unsure about how to accomplish this task. export class DetailsEmployeeComponent implements OnInit{ employeeD ...

Discover properties of a TypeScript class with an existing object

I am currently working on a project where I need to extract all the properties of a class from an object that is created as an instance of this class. My goal is to create a versatile admin page that can be used for any entity that is associated with it. ...

Angular has deprecated the use of `isObject` and `isNullOrUndefined` in TypeScript

Upon upgrading from Angular 7 to Angular 10 and implementing TypeScript 4 or higher, I began receiving deprecation warnings for the functions isObject() and isNullOrUndefined() when running ng lint warnings The function isNullOrUndefined has been depreca ...

Steps for managing files in Ionic Native: creating, reading, and writing them

Struggling to find proper examples for file operations like creating, reading, and writing text or logs into a file? I've done a lot of research but haven't stumbled upon any suitable solutions. The examples provided in this link seem helpful, ho ...

Array of class properties in Typescript

Is there a way to iterate through the properties of a class or a new object instance of that class when none of the properties are set? When I use Object.keys(), it returns an empty array because no properties have been initialized. How can I achieve this ...

The output is displayed on the console, but it cannot be stored in a variable

var x = ""; Promise.all(listOfItems).then(function(results) { for (let j in results) { var newitem = results[j]; x = newitem; console.log(`x: ${x}`); } }); The output on the console displays x: "val ...

Transforming an array of JSON items into a unified object using Angular

I need to convert an array list into a single object with specific values using TypeScript in Angular 8. Here is the array: "arrayList": [{ "name": "Testname1", "value": "abc" }, { "name": "Testname2", "value": "xyz" } ] The desired ...

"An issue of type TypeError occurred: When logging out of the application, it appears that 'x is null

Currently in my app, I am working on implementing authentication following the guidance provided in this example: Click here for more information. However, I have encountered an error that reads "ERROR TypeError: 'x is null'" when trying to execu ...

Creating a string within the component.html file in Angular

Currently, I am utilizing the @ngx Translate Service. Within a template, you can utilize it in the following manner, where 'stringName' represents a key within a JSON file: {{ 'stringName.subStringname' | translate }} The issue I am fa ...

Combining Filter Subject with RxJS for Text Filtering in Angular Material Table with HTTP Results

I'm attempting to implement the example of Angular Material Text Filtering by using the data obtained from an http get request. export class MyDtoDataSource extends DataSource<IMyDto> { private _filterChange = new BehaviorSubject('&a ...

TypeScript error: Cannot find property 'propertyName' in the 'Function' type

I encountered an issue with the TypeScript compiler when running the following code snippet. Interestingly, the generated JavaScript on https://www.typescriptlang.org/play/ produces the desired output without any errors. The specific error message I recei ...

What is a suitable alternative to forkJoin for executing parallel requests that can still complete even if one of them fails?

Is there a more robust method than forkJoin to run multiple requests in parallel and handle failed subscriptions without cancelling the rest? I need a solution that allows all requests to complete even if one fails. Here's a scenario: const posts = th ...

There is no index signature on type '{}' that accepts a parameter of type 'string'. Error code: ts(7053)

Just starting out with react typescript and I've encountered a problem with props and their types. Specifically, this line is causing an error: collapseStates["" + el.name + el.identifier] = true;. The error message reads: "Element implicitl ...

Differences in weekend start and end days vary across cultures

Looking for a solution to determine the weekend days per culture code in Typescript/Javascript? While most countries have weekends on Sat-Sun, there are exceptions like Mexico (only Sunday) and some middle-eastern countries (Fri-Sat). It would be helpful ...

React component not displaying any content due to ternary operator condition being met with variable equal to 0

Seeking to display a React component upon clicking another component. When clicked, I assign the eventKey of the component to a savedId in order to render the corresponding data from the array at that index. Click Action <ListGroup> {data.map((it ...

Updating state atoms in Recoil.js externally from components: A comprehensive guide for React users

Being new to Recoil.js, I have set up an atom and selector for the signed-in user in my app: const signedInUserAtom = atom<SignedInUser | null>({ key: 'signedInUserAtom', default: null }) export const signedInUserSelector = selecto ...

Implement the usage of plainToClass within the constructor function

I have a dilemma with my constructor that assigns properties to the instance: class BaseModel { constructor (args = {}) { for (let key in args) { this[key] = args[key] } } } class User extends BaseModel { name: str ...