I (or rather, a library I'm using) have a lot of interfaces for various options where a lot is optional.
interface Options {
a?: number;
b?: number;
c?: number;
}
I create objects of the options I'm going to use, but also need to refer to these myself. I want the type checking when making the object, but after that I don't want things I've not specified to be available, or things I have specified to be possibly undefined, like here:
const o: Options = { a: 1 } // Good: Object is type checked
o.a.toFixed(0) // Bad: `a` is possibly undefined
Could just skip the type, but then there's no type checking of the options:
const o = { a: 1, d: 2 } // Bad: `d` isn't an option
o.a.toFixed(0) // Good: `a` is defined
So, I created a helper function:
const createOptions = <O extends Options>(options: O): O => options
const o = createOptions({ a: 1 }) // Good: Object is type checked
o.a.toFixed(0) // Good: `a` is defined
This works, but having to create a function like this for every option type is getting annoying and messy. Is it possible to create a single generic helper function for this?
My first newb attempt was the following, but here Typescript requires me to supply 2 types, rather than just the 1 (Options
) that should be necessary.
const create = <T, U extends T>(obj: U): U => obj
const o = create<Options>({ a: 1 }) // Bad: Typescript wants me to specify U
How can I write this identity function so I only need to specify T
, and have Typescript infer U
itself from the object I pass in?
Playground Link
Because of the lack of partial type argument inference, currently is all or nothing: you have to either provide all type arguments explicitly or leave the task entirely to TS.
Anyway there is a workaround for that:
function create<T extends object>() {
return <U extends T>(obj:U) => obj
}
const o3 = create<Options>()({ a: 4 })
It is not very beautiful, but it does its job.
What about the "I don't want things I've not specified to be available" part? Well, for the TS point of view, an object like { a: 4, d: "hi" }
does extends (is assignable) to Options
.
So, you can use a utility like the following:
type ExtractImplementedKeys<ST, T> = {
[K in Extract<keyof T, keyof ST>]: ST[K]
}
function create<T extends object>() {
return <U extends T>(obj:U): ExtractImplementedKeys<U, T> => obj
}
You will still be able to create an object like:
const o3 = create<Options>()({ a: 4, d:"Hi" })
But then you won't be able to use excess properties:
o3.d; // <-- Error
Another strategy is the following:
type StrictExtendCheck<ST, T> = keyof ST extends keyof T ? ST : never
function create<T extends object>() {
return <U extends T>(obj:StrictExtendCheck<U, T>) => obj
}
Then, something like:
const o3 = create<Options>()({ a: 4, d:"Hi" })
Will raise an error, unfortunately not a very descriptive one.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With