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` ignoredtype R2 = IsNever<1, { distributive: false; $else: 'E' }> // compiled, `distributive` is not an option of IsNeverBoth 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.
What breaks
Section titled “What breaks”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.
Your type takes the same options
Section titled “Your type takes the same options”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 }> // falsetype E = Mine<{}, { exactt: true }> // error: 'exactt' is not a valid option. Did you mean 'exact'?Your type takes its own options
Section titled “Your type takes its own options”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 }> // falsetype R2 = IsNonEmptyObject<{ a: 1 }, { nonEmpty: true; selection: 'filter' }> // { a: 1 }type E1 = IsNonEmptyObject<{}, { nonEmptyy: true }> // errortype E2 = IsNonEmptyObject<{}, { nonEmpty: true; exactt: true }> // errorKeep 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 }> // nevernonEmpty 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> : neverDeclaring strict options on your own type
Section titled “Declaring strict options on your own type”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 |