Typescript keyof overlap of Partial

4.7k views Asked by At

I would like to type a DB insert function that allows defaults: (I am using SetOptional of type-fest)

function defaultInsert<T, D = Partial<T>>(data: SetOptional<T, keyof D>, defaults: D) {

What I am trying to say with the SetOptional is that the data object should contain anything in T, not specified in defaults (D) but it is allowed to overwrite anything in the defaults.

Sadly this is resulting in the following error

Type 'keyof D' does not satisfy the constraint 'keyof T'.
  Type 'string | number | symbol' is not assignable to type 'keyof T'.
    Type 'string' is not assignable to type 'keyof T'.(2344)

Typescript playground for a more elaborate example.

Is there any way I can filter down the Partial to only contain keys of T since I'm assuming this is the problem due to the possibility of D containing excess properties not in T?

Many thanks

EDIT: As a suggestion from @Alex Chashin, another TS Playground using the keys as a way to define the defaults.

EDIT2: Updated playground with solution provided in UPD2 @Alex Chashin

EDIT3: Updated the previous example so that defaults can also be omitted: playground

1

There are 1 answers

4
Alex Chashin On BEST ANSWER

UPD2: So the only thing I could come up with, that is not too complicated to exist in this world is this (ts playground):

function defaultInsert<T, D extends keyof T>(
  data: Omit<T, D> & Partial<Pick<T, D>>,
  defaults: Pick<T, D>
): void {
  // Hover over this variable to see its type
  const doc = { ...data, ...defaults } as T
  insert<T>(doc)
}

This passes tests you provided, but unfortunately {...data, ...default} is not inferred to be T, so you have to cast it. I don't think it's a big deal though, as this type cast takes only 4 symbols. The only problem is that you have to explicitly provide keys for D every time as you need in example writing keyof typeof personDefaults UPD: Link to TS playground with one possible solution: playgroung

Original answer:

So Partial<T> already makes all keys of T optional, you don't need to use SetOptional after you did Partial. In case you want all data keys to be optional just use this:

function defaultInsert<T>(data: Partial<T>, defaults: T) {
  // ...
}

Notice that defaults has type T, not Partial<T>, because if it was Partial<T> you could provide document that is not complete as data and document that is not complete as defaults, so merging them would not give a complete document, which could be a problem for you. If it isn't, replace T with Partial<T>, it's hard to judge, because you didn't provide all of the code.

If you only want some keys to be optional, do this:

function defaultInsert<T, Keys extends string>(
  data: SetOptional<T, Keys>, 
  defaults: { [key in Keys]: T[key] }
) {
  // ...
}

Like this you will only need to provide defaults for optional keys but not for all ones You could also make defaults of type

Omit<Partial<T>, Keys> & { [key in Keys]: T[key] }
// or if you use your `SetOptional`
SetOptional<T, Exclude<keyof T, Keys>>

So that typescript doesn't complain, when you provide defaults for keys that are not optional

then use like

defaultInsert<{ foo: number, bar: string }, 'foo'>(
  { bar: 'abc' },
  { foo: 12345 }
)