Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: [DeepPartial] Support for optionally excluding user-provided types. #361

Open
PieterDexper opened this issue Mar 20, 2023 · 7 comments
Labels
enhancement New feature or request v10.1

Comments

@PieterDexper
Copy link

PieterDexper commented Mar 20, 2023

What

I propose to add an additional optional generic parameter to DeepPartial (and possibly other similar types) that allows the user to specify their own types that should be excluded from being made (Deep)Partial on top of the already built in exceptions.
DeepPartial<Type, Exclude>

Any nested type matching Exclude would not be made recursively partial.

This is helpful in cases where the system has non-standard built in types that can never be partially present, which currently can't be excluded from DeepPartial without fully re-implementing it.

Examples

type Example = {
   foo: Timestamp;
}

const a: DeepPartial<Example> = ...
const b: DeepPartial<Example, Timestamp> = ...

const timestampA = a.foo?.toMillis() // Error, toMillis may be undefined.
const timestampA = a.foo?.toMillis?.() // Works, but ugly. 
const timestampB = b.foo?.toMillis() // No error, Timestamp was excluded from DeepPartial. 

Additional Info

I'm unsure if this meets the standards of simplicity and general usability, would this be a welcome addition?

@PieterDexper PieterDexper added the enhancement New feature or request label Mar 20, 2023
@Beraliv
Copy link
Collaborator

Beraliv commented Mar 21, 2023

Hey @PieterDexper 👋

Thanks for the issue!

In your specific example you can simply use MarkOptional – https://tsplay.dev/N55DPN

If you want to do it on more than 1 layer, I'm happy to create a separate DeepMarkOptional

Will it be enough for your use case?

@Beraliv Beraliv added the awaiting response Awaiting response from the person who created an issue label Mar 21, 2023
@PieterDexper
Copy link
Author

Hi @Beraliv

Thanks for the input! I'm not sure a recursive version of MarkOptional won't result in the same problem.

A quick check with:

MarkOptional<Timestamp, keyof Timestamp>

results in its methods giving the same error.

If I understand it correctly with a recursive version of MarkOptional I would be able to use this to exclude specific keys from being made optional, which would be an improvement because I could exclude Timestamp's toDate and toMillis from the checks recursively, and then ensure they are not in use as key names anywhere else, or alternatively standardize the names of Timestamp fields and exclude those instead.

This isn't ideal because the full setup I have uses DeepPartial within integration tests on data models that describe everything in our system, which results in the issue above with the built in Timestamp type. Since it's built on a document-based database the timestamps can appear within the types at any level of nesting, with arbitrary key names. Those keys may also be in use elsewhere containing a different type.

My workaround at the moment re-implements DeepPartial with this added to the various checks in there:

type SelectiveDeepPartial<Type> = Type extends Timestamp 
    ? Type
    : //rest of checks from DeepPartial 

@Beraliv
Copy link
Collaborator

Beraliv commented Mar 21, 2023

Currently ts-essentials doesn't have a way to specify what will be excluded and what not.

Currently https://github.com/ts-essentials/ts-essentials/blob/master/lib/deep-partial/index.ts#L5 there are the following types that aren't extended:

  • Builtin (except for Error now)
  • unknown

This isn't ideal because the full setup I have uses DeepPartial within integration tests on data models that describe everything in our system, which results in the issue above with the built in Timestamp type

I see it now. Can you make up the simple example that explains what you're doing in tests? That will be important to mention so people will understand why it's designed this way and what use cases it covers.

Speaking of implementation, I think that can be changed with default generic type.

Let's say we will have two generic types now DeepPartial<Type, NonExtendableMembers = Exclude<Builtin, Error>> = ... but if you want to extend it, you will be able to change it to DeepPartial<Type, Exclude<Builtin, Error>> | Timestamp

I will think whether I want it to be configurable, maybe I will have generic type differently if anything needs to be added in the nearest future, e.g. DeepPartial<Type, { extendable: Error; nonExtendable: Builtin | Timestamp }, so backwards compatibility won't be broken

Quick PoC in here – https://tsplay.dev/mZ1x1N (extendable doesn't do anything for now, but it can do something for Primitives/Builtin types)

To note, Similarly to include and exclude in tsconfig.json, exclude will be applied first, then include

I'm also open for the feedback about names of the configuration, it can be include/exclude, extends/ignores (clearer intention) or anything else, please let me know

There's another related issue where property key is omitted recursively, you might be interested to have a look at it – #104

What do you think?

@PieterDexper
Copy link
Author

PieterDexper commented Mar 21, 2023

Providing a config parameter with a default type to fill it seems like an excellent solution. Maximum flexibility with what gets (or doesn't get) included.

A less flexible but slightly simpler version could be something like DeepPartial<Type, Exclusions=never>, only allowing additional exclusions but not otherwise modifying the defaults.


My use case is a little complex:

The system is built on Firestore, which is a document-based noSql database where both frontend and backend talk directly to the database.

To support this we have a data model library that contains Typescript types describing the content, as well as the database paths it can be found on. This is combined with a wrapper around the Firestore SDK that uses this information to type the various database interactions.

//someData's typing is automatically inferred from the path. 
const someData = await database.someSpecificThing("somePathParameter", "documentId").get();

// The type of the input to 'set' is inferred from the path.
await database.someOtherThing("documentId").set({
   //content
});

This ensures the various applications are always in sync on what & where stuff is in the database, provides nice editor support and also avoids the constant casting and specifying generic parameters that the SDK normally requires to support types.

Within integration tests we rarely want to completely fill out every single field, most functionality only relies on some of it, so to avoid clutter in the setup, or constant usage of explicit as (Deep)Partial<Blabla> in the tests we have a test version of the wrapper that makes all data models DeepPartial

This results in problems with the databases built in data types (like Timestamp) these can never be partially present and should be excluded from DeepPartial everywhere, at any level of nesting and under arbitrary (non-unique) keys.


Perhaps for the example something shorter is better, I reckon this situation applies to any system that has their own representations of basic data types:

Example:
Some systems have built in types or special representations of specific types of data. When these are used within models they are effectively primitive types.

The configuration parameter to DeepPartial makes it possible to exclude these types from being modified recursively:

type MySystemExclusions =  DefaultDeepPartialConfiguration & {nonExtendable: SpecialTypeOne | SpecialTypeTwo}
const partialData: DeepPartial<MyModel, MySystemExclusions> = //...

@Beraliv Beraliv removed the awaiting response Awaiting response from the person who created an issue label Mar 22, 2023
@WillsterJohnson
Copy link

My two cents on the nonExtendable - I think a more obvious name for it would be something like nonPartial or more verbosely excludeFromPartial. Users can then be fully assured from the name alone that DeepPartial will not touch any values with a matching type to the given union. Something like nonExtendable is a little too obscure, it doesn't immediately strike me as meaning "these things will be left alone".

@goldingdamien
Copy link

※ I think this is part of the same issue, if it is not please let me know and I'll make a new issue.
DeepPartial is changing arrays like below:

key?: string[]
=>
key?: (string|undefined)[] | undefined

How can I have string instead of (string|undefined) ?

@Beraliv
Copy link
Collaborator

Beraliv commented Apr 7, 2024

※ I think this is part of the same issue, if it is not please let me know and I'll make a new issue.

DeepPartial is changing arrays like below:

key?: string[]

=>

key?: (string|undefined)[] | undefined

How can I have string instead of (string|undefined) ?

@goldingdamien correct, it is recursively called for all types including arrays so if you'd like to prevent it, there has to be an option to disable it for arrays so arrays stay as is.

I am considering the option that will turn on and off specific types by the names (e.g. date, error, array, etc)

I will keep you updated when I have a draft version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request v10.1
Projects
None yet
Development

No branches or pull requests

4 participants