#function typing: generate interface with optional partial

45 messages · Page 1 of 1 (latest)

glacial linden
#

Hi there, I am trying to create a function that returns some interface and takes in an optional partial of the interface as its argument.

I would like the function's return type to reflect the partial passed in. For instance, in the following code, foo.b should be of type string, not string | null.

interface Foo {
    a: string,
    b: string | null
}

function genFoo(partial?: Partial<Foo>): Foo {
    const foo= {
        a: 'something random',
        b: 'another random thing'
    }
    return {...foo, ...partial}
}

const foo = genFoo({b: 'not null'})
foo.b.toUpperCase() // Type error!

I tried the following but with no success.

interface Foo {
    a: string,
    b: string | null
}

function genFoo<T extends Partial<Foo>>(partial?: T): Foo & T {
    const foo= {
        a: 'something random',
        b: 'another random thing'
    }
    return {...foo, ...partial}  // Type error!
}

const foo = genFoo({b: 'not null'})
foo.b.toUpperCase()

It works if I make partial required, genFoo(partial:T), but how can I make it work while keeping partial optional?

I also experimented with function overloading and conditional return types, but things got complicated really quickly. I feel like I might be missing something obvious. I looked on SO and here and couldn't find any similar questions.

Any help would be greatly appreciated!

Playground link:

#1200132102277050460 message

hoary masonBOT
#
avisca#0

Preview:```ts
// interface Foo {
// a: string,
// b: string | null
// }

// function genFoo(partial?: Partial<Foo>): Foo {
// const foo= {
// a: 'something random',
// b: 'another random thing'
// }
// return {...foo, ...partial}
// }
...```

lapis galleon
#

@glacial linden Here's a version that works

#
function genFoo(partial?: Partial<Foo>) {
    const foo= {
        a: 'something random',
        b: 'another random thing'
    }
    return {...partial, ...foo}
}
#

Two changes:

  1. Removed the return type annotation, since it's not specific enough. It says that it returns Foo but that is saying that b can be null because that's part of Foos type.
#
  1. And I also changed the spread order. ... though, hmm, I guess that's the wrong runtime behavior.
glacial linden
#

@lapis galleon , It does not make sense to overwrite the partial with the generated data though

#

It works because it "know" the generated b is of type string, but if I was doing reall data gen and it could be null, then it wouldn't work.

lapis galleon
#

Yeah, realized that. The issue here is that they can call genFoo({ b: null }) and that will return a null b.

#

This seems to work:

type RemoveNulls<T> = {
  [K in keyof T]: NonNullable<T[K]>
}

function genFoo(partial?: Partial<RemoveNulls<Foo>>) {
    const foo= {
        a: 'something random',
        b: 'another random thing'
    }
    return {...foo, ...partial}
}
glacial linden
#

Exatly. Also, I left that out for simplicity, but I am actually doing this with deep partials meaning my partial is deeply merged with the generated data. Not sure if that changes anything, but I am trying to wrap my mind around the easy case first.

#

But the code should be able to take b: null, here you are simply forcing the partial to pass a b:string no?

lapis galleon
#

Yeah, which depending on what you're doing is probably reasonable.

glacial linden
#

No, because this is used for generating testing data. In most cases I just want random data, but in some cases I want to force specific fields with whatever valid value. That is, I could have a case where I want to force b: null and other cases where I want to force b: string

lapis galleon
#

I guess you'll probably have to find some solution with generics then.

glacial linden
#

I am not sure what you mean. Isn't T in my post above a generic?

lapis galleon
#

Sorry forgot that you gave that example.

#

But yeah, I think that's close, I think this works:

#
function genFoo<T extends Partial<Foo>>(partial?: T) {
    const foo= {
        a: 'something random',
        b: 'another random thing'
    }
    return {...foo, ...partial} as Omit<typeof foo, keyof T> & T
}
#

Spreading isn't quite the same as & so you can't just use Foo & T.

glacial linden
lapis galleon
#

And you want typeof foo not Foo because otherwise genFoo({}) claims that b can be null.

lapis galleon
glacial linden
#

But why is that this is so easy to accomplish if partial is required, but practically impossible if it is optional? I can't wrap my mind around that.

lapis galleon
#

Probably because this is valid:

genFoo<{ lol: "whatever" }>();
#

(Well, okay that complains about lacking properties in common, const foo = genFoo<{ lol: 'whatever', b: "bar" }>() is legal though)

#

Optional generics are tricky that way - if the parameter is omitted the generic can still be provided and that can effect the type.

glacial linden
#

So what is typescript doing when you don't provide the parameter? It still considers the generic or simply throws it out?

Like if you have foo<T>(param?: T): T and call foo(), I assume it still returns T?

#

Does that mean the idiomatic way of doing what I am trying to do is with function overloading?

lapis galleon
#

Yeah, if TS can't infer a type parameter it just defaults it.

#

A type parameter defaults to its constraint, if there is one, otherwise unknown.

#

But they can be manually specified.

#

So yeah, foo() would be unknown, but foo<"magic">() is "magic".

#

So yeah, optional generics are often better to do with overloading so there's only the generic in the overload signature where it's actually specified.

#

But that's just a different flavor of unsafe.

glacial linden
#

I see!

But that's just a different flavor of unsafe.

I never realized that, I though function overloading was type safe since the implementation has to be compatible with all signatures. How is it unsafe?

lapis galleon
#

TS only does basic checking that the signatures are hypothetically compatible.

#

It doesn't verify that the implementation actually matches what the overloads say that it does.

#
function foo(x: string): string;
function foo(x: number): number;
function foo(x: string | number): string | number {
  return "oops"
}
hoary masonBOT
#
function foo(x: string): string;
function foo(x: number): number;
//       ^^^
// This overload signature is not compatible with its implementation signature.
function foo(x: string): string {
  return "oops"
}
glacial linden
lapis galleon
#

Yup. It's up to the writer to make sure that the overloads accurately describe the behavior.

glacial linden
#

Okay, well thank you for the help sir! I'll take another stab at this some other day. I think I better understand why when the partial is optional there is no way around helping the typing system. But I'll need more time to properly understand how to make this work nicely I think.

lapis galleon
#

Yup. Also depends on your real case; I feel like in this case making it mandatory and just having to specify genFoo({}) instead of genFoo() might be a small price to pay for a bit less annoying typings; but if you're doing more complex nested stuff maybe not.

#

Though I'd probably consider nesting function calls rather doing complex deep logic in one function call:

genFoo({
   bar: genBar({ baz: 0 })
})

rather than having some 'deep' defaults for bar inside genFoo