TypeScript Conditional Type Inference in Function Parameters

Conditional Types are type-level conditional expressions of the form:

SomeType extends OtherType ? TrueType : FalseType;

TypeScript can infer types for expressions in many cases, but it seems to be unable to do so for generic functions with type parameters involving conditional types, at least in version 4.7.4. This post illustrates when such type inference could be useful and how to nudge TypeScript to infer types correctly.

When Conditional Type Inference Does not Happen

The problem arises when we have a generic function with a type parameter whose type involves a conditional type:

const inferPlease = <Something>(
  param: Something extends infer Whatever ? Whatever : never
) => param;
const result = inferPlease("easy"); // result:unknown

TypeScript fails to infer here that the type parameter Something is string literal type 'easy'.

type InferPlease<Something> = Something extends infer Whatever
  ? Whatever
  : never;
// `ShouldBeEasy` is an alias for string literal type `'easy'`
type ShouldBeEasy = InferPlease<"easy">;

There is probably an interesting reason why TypeScript does not infer param:'easy' in the snippet above, and the issue "Inference failing for conditional types in function parameters" in TypeScript's repo may be related.

Using Conditional Type for a Configuration Object

Suppose we have a static configuration object, a collection of records where each entry may have a slightly different object type for property params, and an optional property optional:boolean.

const config = {
  foo: {
    optional: true,
    params: {
      a: 1,
    },
  },
  bar: {
    params: {
      b: 2,
    },
  },
};

In the code abovem, the property optional is intended to be optional but defined in each entry. However, TypeScript infers the following type for config.

const config: {
  foo: {
    optional: boolean;
    params: {
      a: number;
    };
  };
  bar: {
    params: {
      b: number;
    };
  };
};

Trying to access the property optional in a generic function given a config entry key of type keyof typeof param gives an error:

const isOptionalEntry = <ConfigKey extends keyof typeof config>(
  configKey: ConfigKey
) => {
  // Property 'optional' does not exist on type '{ optional: boolean; params: { a: number; }; } | { params: { b: number; }; }'.
  // Property 'optional' does not exist on type '{ params: { b: number; }; }'.ts(2339)
  return config[configKey].optional;
};

In this case, we would like config to have the following type.

const config: {
  foo: {
    optional?: boolean;
    params: {
      a: number;
    };
  };
  bar: {
    optional?: boolean;
    params: {
      b: number;
    };
  };
};

Specifying the type manually for config is tedious and should be inferred instead. We can use a passthrough function with a conditional type to tweak the initially inferred type.

const addOptionalProperty = <Config extends Record<string, unknown>>(config: {
  [Key in keyof Config]: Config[Key] extends infer ConfigEntry
    ? ConfigEntry & { optional?: boolean }
    : never;
}) => config;

Like earlier, TypeScript cannot infer the type for parameter config here.

const config = addOptionalProperty({
  foo: {
    optional: true,
    // Type '{ optional: true; params: { a: number; }; }' is not assignable to type '{ optional?: boolean | undefined; }'.
    // Object literal may only specify known properties, and 'params' does not exist in type '{ optional?: boolean | undefined; }'.ts(2322)
    // ...: The expected type comes from property 'foo' which is declared here on type '{ foo: { optional?: boolean | undefined; }; bar: { optional?: boolean | undefined; }; }'
    params: {
      a: 1,
    },
  },
  bar: {
    // Type '{ params: { b: number; }; }' is not assignable to type '{ optional?: boolean | undefined; }'.
    // Object literal may only specify known properties, and 'params' does not exist in type '{ optional?: boolean | undefined; }'.ts(2322)
    // ...: The expected type comes from property 'bar' which is declared here on type '{ foo: { optional?: boolean | undefined; }; bar: { optional?: boolean | undefined; }; }'
    params: {
      b: 2,
    },
  },
});

Helping TypeScript to Infer Conditional Type in Generic Function

To circumvent the problem with inferring conditional type in a generic function type parameter, we can assign the value to an identifier and use typeof keyword to communicate type information.

const config_ = {
  foo: {
    optional: true,
    params: {
      a: 1,
    },
  },
  bar: {
    params: {
      b: 2,
    },
  },
};

const config = addOptionalProperty<typeof config_>(config_);

Now config has the following type:

const config: {
  foo: {
    optional: boolean;
    params: {
      a: number;
    };
  } & {
    optional?: boolean | undefined;
  };
  bar: {
    params: {
      b: number;
    };
  } & {
    optional?: boolean | undefined;
  };
};

and the function isOptionalEntry no longer causes type errors:

const isOptionalEntry = <ConfigKey extends keyof typeof config>(
  configKey: ConfigKey
) => config[configKey].optional;
Tero Laitinen

Tero Laitinen

Helsinki, Finland