I'm working with a library that provides a deeply optional interface.
type Option = number | {
x?: number;
y?: number;
z?: number;
}
interface Options {
a?: Option;
b?: Option;
c?: Option;
d?: Option;
}
function initializeLibrary(options: Options) {
// ...
}
I want to declare a sparse set of options, use the values myself, then pass the options into the library.
// 1. Declare a sparse set of options.
const opts = {
a: 3,
b: 4,
c: { x: 3 },
};
// 2. Use the option values myself.
console.log("Sum:", opts.a + opts.c.x);
// 3. Pass the options into the library.
initializeLibrary(opts);
This works, but it isn't type-safe. I can add keys to opts
that don't belong to Options
, and I won't get an error.
const opts = {
a: 3,
f: 3, // `f` doesn't belong to Options.
};
initializeLibrary(opts); // No error!
If I declare opts
with type Options
, then it becomes type-safe, but I get an error when I access opts.a
.
const opts : Options = {
a: 3,
b: 4,
c: { x: 3 },
};
console.log(opts.a + opts.c.x); // Error: Object is possibly 'undefined'
How can I declare my options in a type-safe way while still having access to the values?
My best effort is to re-declare the specific parts of the Options
interface that I'm using.
interface MyOptions extends Required<Pick<Options, "a" | "b" | "c">> {
a: number;
c: { x: number };
}
const opts: MyOptions = {
a: 3,
b: 4,
c: { x: 3 },
};
This works; it will protect me from adding properties that don't exist in Options
. But my actual use case is much more complex so I want to trick the computer into doing this work for me.
You can use the satisfies
keyword as of v4.9 :)
const opts = {
a: 3,
b: 4,
c: { x: 3 },
} satisfies Options;
console.log(opts.a + opts.c.x); // all good
initializeLibrary(opts); // all good
const opts = {
a: 3,
b: 4,
c: { x: 3 },
f: 3, // will result in an error
} satisfies Options;