Skip to content

Unknown option keys and generic wrappers

In type-plus 8, every type that takes an options parameter $O rejects keys it does not declare. Before, a misspelled key compiled whenever a valid key sat next to it:

type R1 = IsObject<{}, { distributive: false; exactt: true }> // compiled, `exactt` ignored
type R2 = IsNever<1, { distributive: false; $else: 'E' }> // compiled, `distributive` is not an option of IsNever

Both are errors now:

error TS2344: Type '{ distributive: false; exactt: true; }' does not satisfy the constraint '$StrictOptions<{ distributive: false; exactt: true; }, $Options>'.
Type '{ distributive: false; exactt: true; }' is not assignable to type '{ exactt: "'exactt' is not a valid option. Did you mean 'exact'?"; }'.
Types of property 'exactt' are incompatible.
Type 'true' is not assignable to type '"'exactt' is not a valid option. Did you mean 'exact'?"'.

The suggestion appears when a valid key starts with the unknown key, or the unknown key starts with a valid one. A changed letter, such as $thn for $then, gets the message without a suggestion.

testType checks the same way: testType.string<'a', { exactt: true; exact: true }>(false) is an error.

A generic type that passes its own $O to a type-plus type no longer compiles:

type Mine<T, $O extends IsObject.$Options = {}> = IsObject<T, $O>
// error TS2344: Type '$O' does not satisfy the constraint '$StrictOptions<$O, $Options>'.

This is a TypeScript limit, not a bug. $O extends IsObject.$Options does not stop $O from having extra keys, because TypeScript has no exact object types. So TypeScript cannot prove that $O has no unknown keys.

Pick the shape below that matches your type.

Repeat the strict constraint. The error message does not name the checked type, so two types with the same options type have the same constraint:

import type { $StrictOptions, IsObject } from 'type-plus'
type Mine<T, $O extends $StrictOptions<$O, IsObject.$Options> = {}> = IsObject<T, $O>
type R = Mine<{}, { exact: true }> // false
type E = Mine<{}, { exactt: true }> // error: 'exactt' is not a valid option. Did you mean 'exact'?

Constrain $O with your own options type. Pass it on through $ForwardOptions, which keeps only the keys the target type accepts:

import type { $ForwardOptions, $StrictOptions, IsObject } from 'type-plus'
interface IsNonEmptyObject$Options extends IsObject.$Options {
nonEmpty?: boolean
}
type IsNonEmptyObject<T, $O extends $StrictOptions<$O, IsNonEmptyObject$Options> = {}> =
$O['nonEmpty'] extends true
? keyof T extends never ? false : IsObject<T, $ForwardOptions<$O, IsObject.$Options>>
: IsObject<T, $ForwardOptions<$O, IsObject.$Options>>
type R1 = IsNonEmptyObject<{}, { nonEmpty: true }> // false
type R2 = IsNonEmptyObject<{ a: 1 }, { nonEmpty: true; selection: 'filter' }> // { a: 1 }
type E1 = IsNonEmptyObject<{}, { nonEmptyy: true }> // error
type E2 = IsNonEmptyObject<{}, { nonEmpty: true; exactt: true }> // error

Keep the strict constraint on your own $O. $ForwardOptions drops every key the target does not accept, so without it a typo is dropped silently.

When one of your options shares a name with an option of the target, and must not be forwarded, name it in the third parameter: $ForwardOptions<$O, IsObject.$Options, 'selection'>.

A type that takes a subset of the target’s options, such as testType passing { distributive?: boolean; exact?: boolean } to IsObject, uses the same shape.

Do not narrow without removing your own options

Section titled “Do not narrow without removing your own options”

This narrowing compiles, but answers never whenever a caller passes one of your own options:

type Broken<T, $O extends $StrictOptions<$O, IsNonEmptyObject$Options> = {}> =
$O extends $StrictOptions<$O, IsObject.$Options> ? IsObject<T, $O> : never
type R = Broken<{ a: 1 }, { nonEmpty: true }> // never

nonEmpty is not a key of IsObject.$Options, so the condition is false. TypeScript reports nothing. Use $ForwardOptions instead.

A conditional narrowing is safe only when your type has no options of its own:

type Narrow<T, $O extends $Selection.Options = {}> =
$O extends $StrictOptions<$O, IsObject.$Options> ? IsObject<T, $O> : never

Use $StrictOptions on a type of your own to get the same check and message:

type YourType<T, $O extends $StrictOptions<$O, YourType.$Options> = {}> = ...
namespace YourType {
export interface $Options extends $Selection.Options, $Distributive.Options {}
}

$ErrorMessage<M> is the string type the message is built from. It ends with an invisible zero-width space, so no value satisfies it by accident. Use it for other constraint messages of your own.

The constraint adds about 3 type instantiations per use without options, and about 15 with options. Measured on TypeScript 6.0 and 7 with pnpm --filter type-plus bench:instantiations:

Use Before After
IsObject<T> 20.8 23.8
IsObject<T, { exact: true }> 124.1 140.1
IsNever<T> 11.3 14.3
Assignable<T, object> 34.0 37.0
Assignable<T, object, { distributive: false }> 69.1 84.1