ts-essentials: Unable to use DeepOmit with generic type

export type ProducerOptions = DeepOmit<ProducerDocument, EntityOmit>;

export type ProducerDocument = DeepOmit<
  // eslint-disable-next-line @typescript-eslint/ban-types
  OmitProperties<ProducerAggregateRoot, Function>,
  AggregateOmit
>;
Type 'AggregateOmit' does not satisfy the constraint 'DeepModify<T>'.
  Type 'AggregateOmit' is not assignable to type 'T extends WeakMap<infer K extends object, infer E> ? WeakMap<K, DeepModify<E>> : never'.ts(2344)

I’m repeatedly doing this

export interface Entity<Id> {
  id: Id;
  createdAt: Date;
  modifiedAt: Date;
}

export type EntityOmit = DeepReplace<Entity<never>, never>;
export type AggregateOmit = {
  autoCommit: never;
  [index: symbol]: never;
};

all extend AggregateRoot

this doesn’t work though

export type OmitIt<T extends AggregateRoot> = DeepOmit<
  // eslint-disable-next-line @typescript-eslint/ban-types
  OmitProperties<T, Function>,
  AggregateOmit
>;

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 16

Most upvoted comments

It’s released just now as a part of v10.0.0

The PR https://github.com/ts-essentials/ts-essentials/pull/384 was just merged, the changes will be released by the end of April as major - https://github.com/ts-essentials/ts-essentials/pull/386

There are different approaches that I came up with:

Another type

UnsafeDeepOmit – https://tsplay.dev/WylaKm – analogue for DeepOmit, but there’s not generic constraint applied

Advantages – works with generic type Disadvantages – doesn’t highlight the typo if any, 2 types instead of one

Another type + generic constraint

UnsafeDeepOmit – https://tsplay.dev/W4XEKW + added generic constraint for generic type to force using generic constraint in your case, e.g. Record<"id" | "isLatest" | "checkId" | "createdAt" | "updatedAt", any>

Advantages – works with generic type, highlights the typo if any (more strict) Disadvantages – still 2 types

I’m also happy to rename it to GenericDeepOmit and highlight the usage in documentation


I couldn’t come up with the solution where I can combine both approaches (as it’s unlikely possible, as we cannot use generic constraint for both generic type 1 and filter type)


I’m open for the feedback @Nightbr @xenoterracide , let me know about the name and the approaches that I suggested. Do you have other approaches preferable, could you please explain why?

In case there are some significant changes suggested, that’s what Deep-types currently support:

  1. autocomplete for filter keys (with DeepModify)
  2. filter union support (with DeepModify)
  3. filtering keys for objects in place (key remapping) for readability

I would want to keep this functionality (and it’s covered with tests)

@Nightbr not a problem at all, I made the change major (addressed it in https://github.com/ts-essentials/ts-essentials/pull/384/commits/530b7a78680ba73268e7b3339a3122252e928387) to be absolutely clear

Thanks again for the detailed answer and explanation. Understood the decision and implication, I would still recommend introduce the changes as a major version. In my own use-case, It will work perfectly with our usage (with no BC) so thanks a lot!

I would say it is risky to introduce a BC in existing helpers.

It wouldn’t be a BC as I’m removing a generic constraint on the second type parameter of DeepOmit and DeepPick. It would be a breaking change if it introduces a new TypeError but instead, it’s removing the validating part against a structure of the first parameter type Type which doesn’t affect existing users.

If it’s still treated as a break of contract (which I accept), I can introduce this change as a major version - https://github.com/ts-essentials/ts-essentials/pull/384/files#r1555086601. Aligning names and convention would be a priority right now because it was never done before or if it was done, it wasn’t documented anywhere.

Also what about your first suggestion to add GenericDeepOmit using the prefix Generic* to helpers that supports generics? Would avoid the BC on exisiting helpers.

Generic* prefix would be a new convention and will be only semantic. I find it quite misleading because it also works for non-generic cases. So as a user, I would be very confused about what to use so I would need to go to docs and read it anyway.

Also is strict really the right term to describe not working with generic? Generic is a standard way to type complex object with legit polymorphism use cases and I would not say it’s not strict.

With regard to Strict* prefix, it is used when the additional generic constraint is applied to the utility type parameter, e.g. StrictOmit<Type, Keys> adds a generic constraint for Keys extends keyof Type to be able to pass only existing keys. It will have the same side effect on generic types which is why it was removed from built-in types in TypeScript. The exact term name is to be clarified. Still, given this term is already used in ts-essentials, it would be consistent for users to continue using it instead of adding another convention on top of it.


To add on top of it, one big advantage of my approach (with potential break of contract) is performance (see https://github.com/ts-essentials/ts-essentials/pull/384/files#r1555086934) - DeepPick would avoid a generic constraint which will simplify the conditional types a lot (it’s still not done in the PR but it would cut half of the size and Strict* version would only add one generic constraint). If doing other way round, it wouldn’t achieve the same result.

PR is in progress - https://github.com/ts-essentials/ts-essentials/pull/384, please let me know if you have any input cc @xenoterracide @Nightbr

Thanks a lot for this very detailed exploration. My preference goes to GenericDeepOmit and highlight the usage in documentation.

Add to that, I’m ok that DeepOmit doesn’t work with not typed generic but maybe we could find a way to make it work when generic extend a defined type.

import { DeepOmit } from 'ts-essentials';

type GenericCheckData = {
  id: string;
  isLatest: boolean;
  checkId: string;
  createdAt: Date;
  updatedAt: Date;
}

export type ExposedCheckData<T extends GenericCheckData> = DeepOmit<
  T,
  {
    createdAt: never;
    updatedAt: never;
  }
>;

// Result type should be T without createdAt and updatedAt.

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgEQKarAeRMeBfOAMyghDgHIYBnAWlSqtQDsZgBDAGyvIG4BYAFCCYATzCo4AcWaoowAMYBhABap5Aa2RsYbOAF5EguHGAATAFxwqMOUwDm-AceBUAMtvoxLAIwgQOqGxMjsbyqhoAkhZWNsD2IXDyUIEwqKYAgl4oHgkArmCmHhlZWqmOeIKCqAAekLBwouJwAKK1EIymKmqa2mwAPAAqcDWpTKZUUjJySuE9OgB8+ijoWDgwfUZwAwA0mwiboclFmZZMqABusgnG+YWpxacXV5sVAvOOQA

Did you explore with the extends on Generic?

Thank you for the playground link @Nightbr, I will spend time this week to have a look at it

Same here, cannot be able to DeepOmit on generic:

import { DeepOmit } from 'ts-essentials';

export type ExposedData<T> = DeepOmit<
  T,
  {
    id: never;
    isLatest: never;
    checkId: never;
    createdAt: never;
    updatedAt: never;
  }
>;
Type '{ id: never; isLatest: never; checkId: never; createdAt: never; updatedAt: never; }' does not satisfy the constraint 'DeepModify<T>'.
  Type '{ id: never; isLatest: never; checkId: never; createdAt: never; updatedAt: never; }' is not assignable to type 'T extends WeakMap<infer K extends object, infer E> ? WeakMap<K, DeepModify<E>> : never'.ts(2344)

Here is a TypeScript playground: https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgEQKarAeRMeBfOAMyghDgHIYBnAWlSqtQDsZgBDAGyvIG4BYAFCDUAD0iw4MAJ5hUcAKJiIjACYBhABaoAxgGtkbGGwA8AFQB8cALwp0WHDGOC4cUwBpniTy+AqAXHBMqABuqFD8Ai4+VAAyhvQwAUGh4d5w2lp6AJL+gSFhEVHpUKjxKgCCiXkphVEArmAqZZVJ+amRcHiC5hFAA