Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic identity function in Typescript

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

like image 981
Svish Avatar asked Oct 15 '25 15:10

Svish


1 Answers

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.

like image 71
Andrea Simone Costa Avatar answered Oct 18 '25 08:10

Andrea Simone Costa



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!