Ways to limit the type based on the value of the argument

Is it possible to narrow down the type of the library in the demo code provided? Can we use the factory function to create a new function fnP with an exact type instead of any?

const libOne = {
  p: () => 0,
  q: () => 1,
};
const libTwo = {
  x: () => 'x',
  y: () => 'y',
};

type LibraryType = 'one' | 'two';
type KeyType<T extends LibraryType> = T extends 'one'
  ? keyof typeof libOne
  : T extends 'two'
  ? keyof typeof libTwo
  : never;

function customFactory<T extends LibraryType>(library: T, key: KeyType<T>) {
  // How can we limit the library's type?
  const selectedLib = {
    one: libOne,
    two: libTwo,
  }[library][key];

  return function () {
    return selectedLib();
  };
}

const fnP = customFactory('one', 'p');
fnP(); // How do we ensure the correct type is returned?

Answer №1

In the following discussion, I will refer to the object { a: libraryA, b: libraryB } as libraryMap for clarity:

const libraryMap = {
    a: libraryA,
    b: libraryB
};

If you want a single code block like libraryMap[lib][key]() to have strong typing with a generic type L for lib, then the type of libraryMap needs to be defined in a way that allows the compiler to establish the correlation. This approach is explained in detail in microsoft/TypeScript#47109. It deals with correlated union types and shifting from unions to generics, as discussed in microsoft/TypeScript#30581.

The concept here is that the type of libraryMap should be viewed as a mapped type over a basic key-value structure so that when you use libraryMap[lib], it's seen as an indexed access into that type, establishing the relationship between the type L.

To implement this, we first rename libraryMap temporarily to _libraryMap and infer its type using TypeScript's typeof operator:

const _libraryMap = {
    a: libraryA,
    b: libraryB
}

type _LibraryMap = typeof _libraryMap;

Next, we define LibKey as the mapped type between the keys of libraryMap and the keys of the objects they point to:

type LibKey = { [L in keyof _LibraryMap]: keyof _LibraryMap[L] };

We also create LibRet as the mapped type connecting the keys of libraryMap with the return type of methods within those objects:

type LibRet = { [L in keyof _LibraryMap]:
    ReturnType<_LibraryMap[L][keyof _LibraryMap[L]]>
};

If there are issues with the ReturnType line, consider using the alternative version provided.

Finally, we reconstruct the type of libraryMap as a mapped type over LibKey using both LibMap and LibRet, leveraging _libraryMap and annotating it as LibMap:

type LibMap = { [L in keyof LibKey]: Record<LibKey[L], () => LibRet[L]> };

const libraryMap: LibMap = _libraryMap;

The successful compilation of this last line indicates that our LibMap type aligns with that of _LibraryMap. While it may seem like a lot of type manipulation without much impact, the compiler recognizes the difference - only LibMap maintains the required form for correlation.

Now let's demonstrate this:

function createFactory<L extends keyof LibMap>(lib: L, key: LibKey[L]) {
    const selectedLib = libraryMap[lib][key];

    return function () {
        return selectedLib();
    };
}

This snippet compiles without errors, indicating that the return type of createFactory() is () => LibRet[L]. Therefore, within the function, libraryMap[lib] is interpreted as type LibMap[L], equivalent to

Record<LibKey[L], () => LibRet[L]>
. Since key is of type LibKey[L],
libraryMap[lib][key]</code (or <code>selectedLib
) is understood as () => LibRet[L]. Consequently, selectedLib() returns LibRet[L].

Therefore, calling createFactory() ensures strongly-typed results:

const myFunction = createFactory('a', 'func');
console.log(myFunction().toFixed(1)); // 0.0
// Successful execution, myFunction() treated as number

Access the Playground link to code

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

Excessive recursion detected in the HttpInterceptor module

My application uses JWT tokens for authentication, with a random secure string inside the JWT and in the database to validate the token. When a user logs out, a new random string is generated and stored in the database, rendering the JWT invalid if the str ...

After updating to Angular 7, an error was encountered: "TypeError: Unable to execute map function on ctorParameters"

After updating my Angular project to version 7, I encountered a new issue. When running "ng serve --open" from the CLI, I received the following error message: Uncaught TypeError: ctorParameters.map is not a function at ReflectionCapabilities._own ...

Encountering an error when trying to set data in a Firestore document with a customized JavaScript object: "Invalid data provided for function DocumentReference.set()"

For my initial project, I need help in identifying where the issue lies. Firstly, I have a function that adds data to Firebase: addpost() { let newposts = new Posts( this.addForm.value ) this.postsservice.addPosts(newposts); } Ne ...

What is the process for accessing getBoundingClientRect within the Vue Composition API?

While I understand that in Vue we can access template refs to use getBoundingClientRect() with the options API like this: const rect = this.$refs.image.$el.getBoundingClientRect(); I am now attempting to achieve the same using template refs within the com ...

Referring to a component type causes a cycle of dependencies

I have a unique situation where I am using a single service to open multiple dialogs, some of which can trigger other dialogs through the same service. The dynamic dialog service from PrimeNg is being used to open a dialog component by Type<any>. Ho ...

How Vue3 enables components to share props

Having recently made the switch from Vue2 to Vue3, I find myself a bit perplexed about the best approach for sharing props among multiple components. My goal is to create input components that can share common props such as "type", "name", and so on. Previ ...

Tips for organizing MUI Card effectively within a React application using TypeScript

Struggling to build a React card component with Material-UI (MUI) and TypeScript that represents a car? The card should contain text, an image, checkboxes, a rating, and a button. Here's the code I've come up with so far: import React from ' ...

The call stack size has been exceeded in Next.js, resulting in a RangeError

Currently attempting to deploy my project on vercel.com but encountering an error specifically with 3 pages that have no internal errors. An error occurred while prerendering the page "/applications". For more information, visit: https://nextjs.org/docs/me ...

Expand the ApiGateProxyEvent category from aws-lambda

Looking to enhance the ApiGateProxyEvent of aws-lambda in d.ts. Initial attempts replaced the entire APIGatewayProxyEvent instead of extending it as intended. declare namespace AWSLambda { interface APIGatewayProxyEvent { body: any; user: any; ...

Unable to redirect to another page in React after 3 seconds, the function is not functioning as intended

const homeFunction = () => { const [redirect, setRedirect] = useState<boolean>(false); const [redirecting, setRedirecting] = useState<boolean>(false); const userContext = useContext(UserContext); useEffect(() => { const valu ...

The transformation from className to class attribute does not occur for custom elements in JSX

I recently encountered an issue with one of my React components where the "className" attribute was being converted to "classname" in the resulting HTML, instead of the expected "class" attribute. The component had a custom element definition as follows: ...

Is there a way for me to indicate to my custom component the specific location within an input message where a value should be displayed

I'm currently developing a value selector component in ionic/angular and I've encountered an issue with the message/title that I want to pass to the component. I want to be able to specify where the selected value should appear within the message ...

Having trouble verifying exceptions in TypeScript/Node.js using Chai

I am facing an issue while trying to test a simple function using chai assertion in my TypeScript code. Here is the function I have: public async test1(){ throw (new Error(COUCH_CONNECTION_ERROR.message)); } The definition of COUCH_CONNECTION_ERROR ...

Is it possible for ko.mapping to elegantly encompass both getters and setters?

Exploring the fusion of Knockout and TypeScript. Check out this code snippet: class Person { public FirstName:string = "John"; public LastName: string = "Doe"; public get FullName(): string { return this.FirstName + " " + this.Las ...

Is there a way to position the label to the left side of the gauge?

Is there a way to position the zero number outside the gauge? I'm having trouble figuring out how to do it because the x & y options won't work since the plotLine's position keeps changing. The zero needs to move along with the plotLine and ...

Issues with command functionality within the VS Code integrated terminal (Bash) causing disruptions

When using Visual Studio Code's integrated terminal with bash as the shell, I have noticed that commands like ng and tsc are not recognized. Can anyone shed some light on why this might be happening? ...

Discovering if objects possess intersecting branches and devising a useful error notification

I have 2 items that must not share any common features: const translated = { a: { b: { c: "Hello", d: "World" } } }; const toTranslate = { a: { b: { d: "Everybody" } } }; The code ab ...

Next.js encountered an error when trying to locate the 'net' module while working with PostgreSQL

I'm facing a challenge in my Next.js project while attempting to retrieve all records from a table. The error message I'm encountering is "Module not found: Can't resolve 'net'" with an import trace pointing to multiple files withi ...

What causes an ObjectUnsubscribedError to be triggered when removing and then re-adding a child component in Angular?

Within a parent component, there is a list: items: SomeType; The values of this list are obtained from a service: this.someService.items$.subscribe(items => { this.items = items; }); At some point, the list is updated with new criteria: this.some ...

Change the values of every element in an array

Looking to update the values of every item in an array list? if (country.hostel) { country.hostel.forEach(function (hostel, index) { hostel.room.forEach(function (room, index) { room = {code:{value: BOOKED}}; ...