typescriptvisual-studio-codetypescript-genericsintellisense

Can I make intellisense output more readable for Typescript generic with Omit


Context: I'm exposing a third party library to users, but needed to remove two properties from all functions in the library (we set them automatically). I've got a function to do that (and also run a bootstrap function if provided). It looks like this:

const wrapped = <
  F extends (args: any) => any,
  A extends Omit<Parameters<F>[0], "fs" | "http">
>(
  f: F,
  pre?: (params: A) => void
): ((args: A) => ReturnType<F>) => {
  return (args: A): ReturnType<F> => {
    if (pre) {
      pre(args);
    }

    return f({ fs, http, ...args });
  };
};

Now this seems to work as expected, flags up the correct properties etc.

My issue is when I come to use a function that has been wrapped this way in VSCode, the intellisense output is quite hard to grok at a glance.

To illustrate the problem with a full example:

const wrapped = <
  F extends (args: any) => any,
  A extends Omit<Parameters<F>[0], "foo">
>(
  f: F
): ((args: A) => ReturnType<F>) => {
  return (args: A): ReturnType<F> => {
    const foo = "bar"
    return f({ foo, ...args });
  };
};

const thirdPartyFunc = (args: {foo: string; bar: string}) => {
  console.log(args.foo, args.bar);
}

const wrappedThirdPatyFunc = wrapped(thirdPartyFunc);

thirdPartyFunc({foo: "0", bar: "1"})
/**
Intellisense looks simple:

const thirdPartyFunc: (args: {
    foo: string;
    bar: string;
}) => void
*/

wrappedThirdPatyFunc({bar: "1"})
/**
Intellisense looks ugly (shows Omit, shows keys that have been omitted):

const wrappedThirdPatyFunc: (args: Omit<{
    foo: string;
    bar: string;
}, "foo">) => ReturnType<(args: {
    foo: string;
    bar: string;
}) => void>
 */

I'm worried that this will be a crappy user experience. Is there any change I can make to my code that would get it to a point it's close (if not exactly like) the original intellisense - minus the fields I removed?

I'm not really sure what to try, this is by far the most complex work I've done with generics in TS - so I'm sort of muddling through and experimenting.


Solution

  • TypeScript uses various heuristic rules for determining how to display a type via IntelliSense quick info. If you have a type alias like type Foo<T> = ⋯T⋯, and you write Foo<string>, the compiler has to decide whether to display it as-is, or to substitute string in for T in the definition like ⋯string⋯. And if the substituted type itself has type aliases in it, then there are more decisions to be made. These heuristics have reasonable behavior for a wide variety of use cases but they don't always line up with the intent of the developer. There are various open feature requests to allow for something more flexible or explicit, such as microsoft/TypeScript#45954, but for now we have to just work with what we have.

    In your case, you are seeing the Omit type alias where you'd prefer not to. The easiest way to prevent that is to remove the type alias entirely, and replace it with an in-line equivalent. You could replace it with its definition, but that uses the Pick utility type, and then you'd have to inline that. Instead, I'd use an alternate definition with key remapping as described in microsoft/TypeScript#41383:

    type Omit<T, K> = {[P in keyof T as Exclude<P, K>]: T[P]}
    

    That turns the code into

    const wrapped = <
        F extends (args: any) => any,
        A extends {
            [K in keyof Parameters<F>[0]
            as Exclude<K, "foo">]: Parameters<F>[0][K]
        }
    >(
        f: F
    ): ((args: A) => ReturnType<F>) => {
        return (args: A): ReturnType<F> => {
            const foo = "bar"
            return f({ foo, ...args });
        };
    };
    

    which gives you the desired behavior of

    wrappedThirdPatyFunc({ bar: "1" })
    /**
    const wrappedThirdPatyFunc: (args: {
        bar: string;
    }) => ReturnType<(args: {
        foo: string;
        bar: string;
    }) => void> */
    

    That answers the question as asked. I would go further here and replace F with the the argument and return types directly:

    const wrapped = <A extends { foo?: string }, R>(
        f: (a: A) => R
    ): ((args: { [K in keyof A as Exclude<K, "foo">]: A[K] }) => R) => {
        return args => {
            const foo = "bar"
            return f({ foo, ...args } as A);
        };
    };
    
    const wrappedThirdPatyFunc = wrapped(thirdPartyFunc);
    
    wrappedThirdPatyFunc({ bar: "1" })
    /**
    const const wrappedThirdPatyFunc: (args: {
        bar: string;
    }) => void
     */
    

    Now there's no ReturnType or Parameters in there and the resulting computed and displayed types are simpler.

    Playground link to code