Search code examples
typescriptgenerics

Why does TS nonempty assertion affect function generics inference?


This is my definition of the type and the variable, the LOCALES_KEYS variable is an enum

export const resources = {
  'ja-JP': jaJP,
  'zh-TW': zhTW,
  'en-US': enUS,
  'zh-CN': zhCN,
};

export type Lng = keyof typeof resources;

export type Resources = {
  [K in Lng]: (typeof resources)[K] & {
    [P in LOCALES_KEYS]: P extends keyof (typeof resources)[K]
      ? (typeof resources)[K][P]
      : '';
  };
};

export interface AppContextData extends DefaultData {
  language?: Lng;
  t: <T extends LOCALES_KEYS>(
    key: T,
  ) => Resources[keyof typeof resources][T];
}

const t: AppContextData['t'] = (key) => {
  const lang: Lng = 'en-US';
  return (resources as Resources)[lang][key];
};

When I use t functions, everything works, but when I use t functions and non-empty assertions, generic inference doesn't work

t(LOCALES_KEYS.AVATAR); // The type of mouse hover over the t function is: const t: <LOCALES_KEYS.AVATAR>(key: HOME_KEYS.AVATAR, options?: TOptions) => "" | "Avatar" | "头像"

// Nonempty assertion
t!(LOCALES_KEYS.AVATAR);
// The type of mouse hover over the t function is: const t: <T extends LOCALES_KEYS>(key: T, options?: TOptions) => (({
//   readonly argots: "Argots";
//   readonly meta_desc: "A free chat platform that encrypts conversation information throughout
// the process to protect your security and privacy. No information is collected from you and no
// permissions are required from you. End of chat Clear all records";
//     ... 19 more ...;
//     readonly Invitation_description: "Each invitation link can only invite one user, and the
// invitation is invalid after success";
// } & {
//     ...;

I'm not sure why this is, I tried to search for related content on Google and I couldn't find it.

This is a reproducible snippet of code,When I use '! 'will affect generic inference

const t: <T>(val: T) => T = (val) => {
  return val;
};

t('111'); //  const t: <"111">(val: "111") => "111"
t!('111'); // const t: <T>(val: T) => T

Solution

  • Type inference is not affected at all. The only difference is how TypeScript chooses to display the type when you use IntelliSense. This is working as intended.

    First, let's demonstrate that inference is unaffected:

    const a1 = t('111'); //  const t: <"111">(val: "111") => "111"
    //    ^? const a1: "111"
    const a2 = t!('111'); // const t: <T>(val: T) => T
    //    ^? const a2: "111"
    const a3 = (t)('111'); // const t: <T>(val: T) => T
    //    ^? const a3: "111"
    

    In all three calls above, the output type is the same string literal type "111". That's what you'd expect when you call a function of type <T>(val: T) => T with an argument of type "111", and that's what happens. That means T has been inferred as "111" in all three calls above.


    The only difference is what is displayed in your IDE when you hover over t. And that's because you're looking at different things.

    When you hover over t in t('111'), you're looking at a function call directly. TypeScript decides to show you the type of the function with the type argument specified, like <"111">(val: "111") => "111". Note that that type isn't actually a valid TypeScript type. You can't use "111" as a type parameter name. It's literally just displaying <T>(val: T) => T with "111" substituted for T.

    On the other hand, when you hover over t in t!('111'), you're looking at a function which is not called directly. TypeScript sees t as having the non-null assertion operator applied to it. It's not being called directly, so there's no type argument to substitute for the type parameter. You just see <T>(val: T) => T. If there were a way to ask TypeScript to give you the type of t! when it's being called, you'd presumably see <"111">(val: "111") => "111". But a pointer hovering isn't going to select anything other than the leaves of the abstract syntax tree (AST), so you can't do that.

    And this has very little to do with the non-null assertion operator. Since t is already known to be non-nullish, t! is just the same value and type as t. To prove that, you can observe that the same thing happens with (t). Writing (t)("111") puts the function call at a non-leaf node of the AST and so you don't see "111" when displaying the type of t.


    To recap: TypeScript is behaving as designed. Type inference is completely unaffected by ! or () in the examples above. Only type display is affected, and this is really just a heuristic thing that IDEs do.

    Playground link to code