1

I'm really close, and sure that an experienced typescript developer would solve this really quick.

I want to create a function that receives an object, and an optional function to extend this object.

  • if no argument is given - the object is returned.
  • if extend function is given(given arg is different from undefined) - a new instance of this function with an updated initial object value is returned.
  • the object is also optional. you override the current value of the object if you pass this object.

usage for example:

let f1 = func(undefined, {'x': 10})
f1() // {x:10}
let f2 = func((pos: any) => ({...pos, y: 10}), {'x': 10})  //f2 => () => {x:number,y:number}
let f3 = f2()  //f3 is {x:10,y:10}
f3 // {x:10,y:10}
let f4 = f2((pos: any) => ({...pos, y: 10}),{})
f4() // {y:10}

javascript implementation:

function func(extend?, obj?) {
    if (extend) {
        let newObj = extend(obj)
        return ((extend, obj = newObj) => func(extend, obj))
    } else return obj
}

so far javascript results work great. so I want to create a typescript function that will follow the exact type of this object.

solved thanks to @jcalz see real usecase


my own attempt

this is not a part of the question and it is only to show what I've already tried.

we will need to create a generic type function that returns a new generic if an arg given

type Primitive = bigint | boolean | null | number | string | symbol | undefined;
type PlainObject = Record<string, Primitive>;

type genericFuncType<K extends PlainObject, Tin extends K=K, Tout extends Tin=Tin, T extends ((pos: Tin) => Tout) = undefined> = T extends undefined ? K: genericFuncType<Tout, Tout, Tout> 
let o = {x:10}
type ot = typeof o
to // {x: number}
type t1 = genericFuncType<ot>
t1 // {x: number}
type t2 = genericFuncType<ot,ot,ot,(pos:ot)=>ot>
t2 // a new function should be returned, but instead {x: number} is returned

well I couldn't make this generic work perfectly, but lets try it on the function, typescript func implementation:

function func<K extends PlainObject, Tin extends K, Tout extends Tin, T extends ((pos: Tin) => Tout) = undefined>(extend: T, obj: K = {} as any): genericFuncType<K, Tin, Tout, T> {
    if (extend) {
        let newObj = extend(obj as any)
        return ((extend, obj = newObj) => func(extend,  obj)) as any
    } else return obj as any
}

let f1 = func(undefined, {'x': 10})  //f1 =>
typeof f1 // {x:number} - good
let f2 = func((pos: any) => ({...pos, y: 10}), {'x': 10})  //f2 => () => {x:number,y:number}
typeof f2 // {x:number} - bad

what I'm doing wrong? why I can't return a new function with extended type recursively?

(for reference only - here's a playground for very similar function that works 2 levels deep - but nut recursively deep)

14
  • What actual practical ting are you expecting the generic to do? It sounds like you should just use any Commented Sep 15, 2021 at 12:30
  • I'm expecting typescript to follow the type of the given object over the different recursive instances of this function. any is just useless Commented Sep 15, 2021 at 12:31
  • But why? What does this achieve? Generics are only useful when you want to constrain types. You don't want to do that, you want to allow any type...so any is the right choice. It's much like using the base object in a strongly typed language, i.e. object in C# Commented Sep 15, 2021 at 12:37
  • 1
    If I try that usage example with your function implementation I immediately get a runtime error like f1 is not a function. If I ignore that and try to give typings to the func() function JavaScript implementation, I get this. If that meets your needs, I'll write up an answer. Note that you can't pass in an extend like (pos: any) => ({...pos, y: 10}), which is of type (pos: any) => any... you'll just get any out; if you want the compiler to do something better, you need a generic like <T>(pos: T) => .... Commented Sep 15, 2021 at 21:11
  • 1
    If it doesn't meet your needs, please try to remove any typos, errors, and extraneous stuff (does Primitive and PlainObject meaningfully contribute to the behavior here? The fact that you're dealing with objects and not primitives seems like you could just remove that for the sake of the question; given your implementation. For example, func((x: number) => x + 1, 100) should be fine), so that people who want to help can focus on the issue. Commented Sep 15, 2021 at 21:17

1 Answer 1

1

From looking at the implementation of func(), I think it would be best to make a Func<T> interface as an overloaded set of call signatures which represents the four different ways you can call it or functions it returns (with or without each of the extend and obj parameters) and what the result should be. A Func<T> is holding onto a result of type T that may or may not come out depending on how you call it:

interface Func<T extends object | undefined> {
  (): T;
  <U extends object>(extend: undefined, obj: U): U;
  <V extends object>(extend: (obj: T) => V): Func<V>;
  <U extends object, V extends object>(extend: (obj: U) => V, obj: U): Func<V>;
}

If you call a Func<T> with no obj argument, then it will operate on the value of type T that it's holding onto. But if you call a Func<T> with an obj argument of type U, then it will operate on that U value instead of T.

If you call a Func<T> with no extend callback, then it will just return the value on which its operating; either T (if no obj is passed) or U (if obj is passed).

On the other hand if you call a Func<T> with an extend callback, that callback will be called on the value on which Func<T> is operating... so the extend callback either takes a T (if no obj is passed) or U (if obj is passed). And whatever type V the extend callback returns, Func<T> will return another function of type Func<V>.

Finally, func itself is evidently a value of type Func<undefined>, since the value it is holding onto is just undefined (if you call func() with no arguments, you get undefined out).

That means we can write this:

const func: Func<undefined> = function func(extend?: any, obj?: any) {
  if (extend) {
    let newObj = extend(obj)
    return ((extend: any, obj = newObj) => func(extend, obj))
  } else return obj
}

Note that I've decided to use a lot of the any type in the implementation since I'm not interested in convincing the compiler that func is actually a Func<undefined>. I'm just claiming it is.


Okay, so let's see it in action:

let f1 = func(undefined, { 'x': 10 });
/* let f1: {
    x: number;
} */
console.log(f1.x.toFixed(2)) // "10.00"

let f2 = func(<T,>(pos: T) => ({ ...pos, y: 10 }), { 'x': 10 })
/* let f2: Func<{ x: number;} & { y: number;}> */
const f2r = f2();
/* const f2r: { x: number; } & { y: number; } */
console.log(f2r.y.toFixed(2)) // "10.00"

Looks good; the compiler understand exactly what is happening with f1() and f2().

But there's an important note about how f2 was produced. The extend callback is of type <T>(pos: T) => T & {y: number}. I had to annotate it as such (or at least the input; the compiler could infer the return type). This is necessary for the compiler to know that whatever is input as pos, a value of the same type with an extra numeric y property comes out. If instead I just called func() with the callback as you had annotated it:

let f3 = func((pos: any) => ({ ...pos, y: 10 }), { 'x': 10 })
/* let f3: Func<any> */
const f3r = f3();
/* const f3r: any */

the compiler would infer that the return type of extend is just any, and extend is of type (pos: any) => any. And once this happens it means that f3 is a Func<any>, and the compiler has forgotten everything you cared about. It's not really the compiler's fault, though; annotating things as any tends to do that; any is infectious that way.

And just to show that we can call the returned functions also:

let f4 = f2((pos) => ({ ...pos, y: 10 }), {});
//let f4: Func<{ y: number; }>

let f5 = f4()

// let f5: { y: number; }
console.log(JSON.stringify(f5)) // {"y":10}

Looks good!


Playground link to code

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you! Almost perfect, you can edit your answer to show that your answer works recursively (like the example usage in my questionl), it works you just did not mention it
Sure thing, although I've asked you at least twice to fix that code section so that it does not error out with f1 is not a function. I'm trying to understand how that code relates to your func() implementation. You write //f3 is a function, but it's not, because you created it by calling f2(), which spits out the held object and not another function. If I'm missing something obvious then please help me by editing that example code to something that actually runs with your JS implementation of func.
you are right! fixed! also see edit for real use case.thank you!
In that edit f1() still explodes things, and unfortunately that playground link seems to be misformatted, so there's no real code in it. Still I can probably follow along enough to put that f4() in there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.