Skip to content

Commit

Permalink
feat: βž• add StrictDeepPick and StrictDeepOmit (#384)
Browse files Browse the repository at this point in the history
* feat: βž• StrictDeepPick

* feat: βž• StrictDeepOmit

* docs: πŸ“„ Conventions

* docs: πŸ“„ DeepOmit / DeepPick

* docs: πŸ“„ changeset

* docs: πŸ“„ add example for Strict* convention

* docs: πŸ“„ add limitations to Strict* types

* refactor: πŸš€ DeepOmit improvements

* refactor: πŸš€ DeepPick improvements

* test: πŸ§ͺ generic type for DeepPick and DeepOmit

* docs: πŸ“„ minor => major
  • Loading branch information
Beraliv committed Apr 17, 2024
1 parent b127a8a commit 9935d80
Show file tree
Hide file tree
Showing 17 changed files with 1,678 additions and 1,510 deletions.
6 changes: 6 additions & 0 deletions .changeset/unlucky-camels-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ts-essentials": major
---

Added `StrictDeepOmit` and `StrictDeepPick` that support generic type and removed generic constraint on the second type
parameter of `DeepOmit` and `DeepPick`
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,14 @@ npm install --save-dev ts-essentials
- [`DeepNullable<Type>`](/lib/deep-nullable) - Constructs a type by picking all properties from type `Type` recursively
and include `null` property values for all of them
- [`DeepOmit<Type, Filter>`](/lib/deep-omit) - Constructs a type by picking all properties from type `Type` and removing
properties which values are `never` or `true` in type `Filter`
properties which values are `never` or `true` in type `Filter`. If you'd like type `Filter` to be validated against a
structure of `Type`, please use [`StrictDeepOmit<Type, Filter>`](./lib/strict-deep-omit/).
- [`DeepPartial<Type>`](/lib/deep-partial) - Constructs a type by picking all properties from type `Type` recursively
and setting them as optional, meaning they aren't required. To make properties optional on one level, use
[`Partial<Type>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype)
- [`DeepPick<Type, Filter>`](/lib/deep-pick) - Constructs a type by picking set of properties, which have property
values `never` or `true` in type `Filter`, from type `Type`
values `never` or `true` in type `Filter`, from type `Type`. If you'd like type `Filter` to be validated against a
structure of `Type`, please use [`StrictDeepPick<Type, Filter>`](./lib/strict-deep-pick/).
- [`DeepReadonly<Type>`](/lib/deep-readonly) - Constructs a type by picking all properties from type `Type` recursively
and setting `readonly` modifier, meaning they cannot be reassigned. To make properties `readonly` on one level, use
[`Readonly<Type>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype)
Expand All @@ -113,6 +115,12 @@ npm install --save-dev ts-essentials
- [`DeepWritable<Type>`](/lib/deep-writable) - Constructs a type by picking all properties from type `Type` recursively
and removing `readonly` modifier, meaning they can be reassigned. To make properties writable on one level, use
`Writable<Type>`
- [`StrictDeepOmit<Type, Filter>`](/lib/strict-deep-omit) - Constructs a type by picking all properties from type `Type`
and removing properties which values are `never` or `true` in type `Filter`. The type `Filter` is validated against a
structure of `Type`.
- [`StrictDeepPick<Type, Filter>`](/lib/strict-deep-pick) - Constructs a type by picking set of properties, which have
property values `never` or `true` in type `Filter`, from type `Type`. The type `Filter` is validated against a
structure of `Type`.

### Key types

Expand Down
13 changes: 5 additions & 8 deletions lib/deep-omit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,9 @@ type AnonymousEnglishClass = DeepOmit<
>;
```

It validates that all `Filter` properties exist in type `Type`
It doesn't validate that all `Filter` properties exist in type `Type`:

```ts
// error: Type '{ id: never; students: { id: never; }[]; teacher: { id: never; }; }' does not satisfy the constraint '{ students?: true | DeepModify<{ name: string; score: number; }[]> | undefined; teacher?: true | { name?: true | undefined; yearsOfExperience?: true | undefined; } | undefined; year?: true | undefined; }'.
// Types of property 'students' are incompatible.
// Type '{ id: never; }[]' is not assignable to type 'true | DeepModify<{ name: string; score: number; }[]> | undefined'.
// Type '{ id: never; }[]' is not assignable to type '(true | { name?: true | undefined; score?: true | undefined; } | undefined)[]'.
// Type '{ id: never; }' is not assignable to type 'true | { name?: true | undefined; score?: true | undefined; } | undefined'.
type UnknownEnglishClass = DeepOmit<
EnglishClass,
{
Expand All @@ -40,10 +35,12 @@ type UnknownEnglishClass = DeepOmit<
id: never;
};
}
// ^^^^^^^^^^^^^^^^^^^^^^^^
>;
```

If you'd like type a second type parameter `Filter` to be validated against a structure of a first type parameter
`Type`, please use [`StrictDeepOmit<Type, Filter>`](../strict-deep-omit/).

Useful in functions which cannot access specified properties

```ts
Expand All @@ -66,4 +63,4 @@ takeSurvey(englishClass);
- `DeepOmit` cannot be used when `Type` is generic type – https://github.com/ts-essentials/ts-essentials/issues/343
- `DeepOmit` only limits access to specified properties in your codebase, but doesn't remove them in runtime

TS Playground – https://tsplay.dev/w1EEAw
TS Playground – https://tsplay.dev/wedDdW
41 changes: 10 additions & 31 deletions lib/deep-omit/index.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,46 @@
import { AnyRecord } from "../any-record";
import { Builtin } from "../built-in";
import { DeepModify } from "../deep-modify";

export type DeepOmit<Type, Filter extends DeepModify<Type>> = Type extends Builtin
export type DeepOmit<Type, Filter> = Type extends Builtin
? Type
: Type extends Map<infer Keys, infer Values>
? Filter extends Map<Keys, infer FilterValues>
? FilterValues extends DeepModify<Values>
? Map<Keys, DeepOmit<Values, FilterValues>>
: Type
? Map<Keys, DeepOmit<Values, FilterValues>>
: Type
: Type extends ReadonlyMap<infer Keys, infer Values>
? Filter extends ReadonlyMap<Keys, infer FilterValues>
? FilterValues extends DeepModify<Values>
? ReadonlyMap<Keys, DeepOmit<Values, FilterValues>>
: Type
? ReadonlyMap<Keys, DeepOmit<Values, FilterValues>>
: Type
: Type extends WeakMap<infer Keys, infer Values>
? Filter extends WeakMap<Keys, infer FilterValues>
? FilterValues extends DeepModify<Values>
? WeakMap<Keys, DeepOmit<Values, FilterValues>>
: Type
? WeakMap<Keys, DeepOmit<Values, FilterValues>>
: Type
: Type extends Set<infer Values>
? Filter extends Set<infer FilterValues>
? FilterValues extends DeepModify<Values>
? Set<DeepOmit<Values, FilterValues>>
: Type
? Set<DeepOmit<Values, FilterValues>>
: Type
: Type extends ReadonlySet<infer Values>
? Filter extends ReadonlySet<infer FilterValues>
? FilterValues extends DeepModify<Values>
? ReadonlySet<DeepOmit<Values, FilterValues>>
: Type
? ReadonlySet<DeepOmit<Values, FilterValues>>
: Type
: Type extends WeakSet<infer Values>
? Filter extends WeakSet<infer FilterValues>
? FilterValues extends DeepModify<Values>
? WeakSet<DeepOmit<Values, FilterValues>>
: Type
? WeakSet<DeepOmit<Values, FilterValues>>
: Type
: Type extends Array<infer Values>
? Filter extends Array<infer FilterValues>
? FilterValues extends DeepModify<Values>
? Array<DeepOmit<Values, FilterValues>>
: Type
? Array<DeepOmit<Values, FilterValues>>
: Type
: Type extends Promise<infer Value>
? Filter extends Promise<infer FilterValue>
? FilterValue extends DeepModify<Value>
? Promise<DeepOmit<Value, FilterValue>>
: Type
? Promise<DeepOmit<Value, FilterValue>>
: Type
: Filter extends AnyRecord
? {
[Key in keyof Type as Key extends keyof Filter
? Filter[Key] extends true
? never
: Key
: Key]: Key extends keyof Filter
? Filter[Key] extends DeepModify<Type[Key]>
? DeepOmit<Type[Key], Filter[Key]>
: Type[Key]
: Type[Key];
: Key]: Key extends keyof Filter ? DeepOmit<Type[Key], Filter[Key]> : Type[Key];
}
: never;
12 changes: 4 additions & 8 deletions lib/deep-pick/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,9 @@ type EnglishClassNamesOnly = DeepPick<
>;
```

It validates that all `Filter` properties exist in type `Type`
It doesn't validate that all `Filter` properties exist in type `Type`:

```ts
// error: Type '{ id: never; students: { id: never; }[]; teacher: { id: never; }; }' does not satisfy the constraint '{ students?: true | DeepModify<{ name: string; score: number; }[]> | undefined; teacher?: true | { name?: true | undefined; yearsOfExperience?: true | undefined; } | undefined; year?: true | undefined; }'.
// Types of property 'students' are incompatible.
// Type '{ id: never; }[]' is not assignable to type 'true | DeepModify<{ name: string; score: number; }[]> | undefined'.
// Type '{ id: never; }[]' is not assignable to type '(true | { name?: true | undefined; score?: true | undefined; } | undefined)[]'.
// Type '{ id: never; }' is not assignable to type 'true | { name?: true | undefined; score?: true | undefined; } | undefined'.
type UnknownEnglishClass = DeepPick<
EnglishClass,
{
Expand All @@ -40,10 +35,12 @@ type UnknownEnglishClass = DeepPick<
id: never;
};
}
// ^^^^^^^^^^^^^^^^^^^^^^^^
>;
```

If you'd like a second type parameter `Filter` to be validated against a structure of a first type parameter `Type`,
please use [`StrictDeepPick<Type, Filter>`](../strict-deep-pick/).

Useful in functions which cannot access specified properties

```ts
Expand All @@ -66,7 +63,6 @@ calculateAverageScore(englishClass);

⚠️ Limitations:

- `DeepPick` cannot be used when `Type` is generic type – https://github.com/ts-essentials/ts-essentials/issues/343
- `DeepPick` only limits access to specified properties in your codebase, but doesn't remove them in runtime

TS Playground – https://tsplay.dev/mAJ2PW
39 changes: 10 additions & 29 deletions lib/deep-pick/index.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,47 @@
import { AnyRecord } from "../any-record";
import { Builtin } from "../built-in";
import { DeepModify } from "../deep-modify";

export type DeepPick<Type, Filter extends DeepModify<Type>> = Type extends Builtin
export type DeepPick<Type, Filter> = Type extends Builtin
? Type
: Type extends Map<infer Keys, infer Values>
? Filter extends Map<Keys, infer FilterValues>
? FilterValues extends DeepModify<Values>
? Map<Keys, DeepPick<Values, FilterValues>>
: Type
? Map<Keys, DeepPick<Values, FilterValues>>
: Type
: Type extends ReadonlyMap<infer Keys, infer Values>
? Filter extends ReadonlyMap<Keys, infer FilterValues>
? FilterValues extends DeepModify<Values>
? ReadonlyMap<Keys, DeepPick<Values, FilterValues>>
: Type
? ReadonlyMap<Keys, DeepPick<Values, FilterValues>>
: Type
: Type extends WeakMap<infer Keys, infer Values>
? Filter extends WeakMap<Keys, infer FilterValues>
? FilterValues extends DeepModify<Values>
? WeakMap<Keys, DeepPick<Values, FilterValues>>
: Type
? WeakMap<Keys, DeepPick<Values, FilterValues>>
: Type
: Type extends Set<infer Values>
? Filter extends Set<infer FilterValues>
? FilterValues extends DeepModify<Values>
? Set<DeepPick<Values, FilterValues>>
: Type
? Set<DeepPick<Values, FilterValues>>
: Type
: Type extends ReadonlySet<infer Values>
? Filter extends ReadonlySet<infer FilterValues>
? FilterValues extends DeepModify<Values>
? ReadonlySet<DeepPick<Values, FilterValues>>
: Type
? ReadonlySet<DeepPick<Values, FilterValues>>
: Type
: Type extends WeakSet<infer Values>
? Filter extends WeakSet<infer FilterValues>
? FilterValues extends DeepModify<Values>
? WeakSet<DeepPick<Values, FilterValues>>
: Type
? WeakSet<DeepPick<Values, FilterValues>>
: Type
: Type extends Array<infer Values>
? Filter extends Array<infer FilterValues>
? FilterValues extends DeepModify<Values>
? Array<DeepPick<Values, FilterValues>>
: Type
? Array<DeepPick<Values, FilterValues>>
: Type
: Type extends Promise<infer Value>
? Filter extends Promise<infer FilterValue>
? FilterValue extends DeepModify<Value>
? Promise<DeepPick<Value, FilterValue>>
: Type
? Promise<DeepPick<Value, FilterValue>>
: Type
: Filter extends AnyRecord
? {
// iterate over keys of Type, which keeps the information about keys: optional, required or readonly
[Key in keyof Type as Key extends keyof Filter ? Key : never]: Filter[Key & keyof Filter] extends true
? Type[Key]
: Key extends keyof Filter
? Filter[Key] extends DeepModify<Type[Key]>
? DeepPick<Type[Key], Filter[Key]>
: never
? DeepPick<Type[Key], Filter[Key]>
: never;
}
: never;
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ export * from "./mark-writable";
export * from "./buildable";
export * from "./deep-non-nullable";
export * from "./deep-nullable";
export * from "./strict-deep-omit";
export * from "./deep-omit";
export * from "./deep-partial";
export * from "./strict-deep-pick";
export * from "./deep-pick";
export * from "./deep-readonly";
export * from "./deep-required";
Expand Down
74 changes: 74 additions & 0 deletions lib/strict-deep-omit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
`StrictDeepOmit<Type, Filter>` constructs a type by picking all properties from type `Type` and removing properties
which values are `never` or `true` in type `Filter`

```ts
interface EnglishClass {
students: { name: string; score: number }[];
teacher: {
name: string;
yearsOfExperience: number;
};
year: number;
}

type AnonymousEnglishClass = StrictDeepOmit<
// ^? { students: { score: number }[]; teacher: { yearsOfExperience: number }; year: number }
EnglishClass,
{
students: { name: never }[];
teacher: {
name: never;
};
}
>;
```

It validates that all `Filter` properties exist in type `Type`

```ts
// error: Type '{ id: never; students: { id: never; }[]; teacher: { id: never; }; }' does not satisfy the constraint '{ students?: true | DeepModify<{ name: string; score: number; }[]> | undefined; teacher?: true | { name?: true | undefined; yearsOfExperience?: true | undefined; } | undefined; year?: true | undefined; }'.
// Types of property 'students' are incompatible.
// Type '{ id: never; }[]' is not assignable to type 'true | DeepModify<{ name: string; score: number; }[]> | undefined'.
// Type '{ id: never; }[]' is not assignable to type '(true | { name?: true | undefined; score?: true | undefined; } | undefined)[]'.
// Type '{ id: never; }' is not assignable to type 'true | { name?: true | undefined; score?: true | undefined; } | undefined'.
type UnknownEnglishClass = StrictDeepOmit<
EnglishClass,
{
id: never;
students: { id: never }[];
teacher: {
id: never;
};
}
// ^^^^^^^^^^^^^^^^^^^^^^^^
>;
```

If you don't need a second type parameter `Filter` to be validated against a structure of a first type parameter `Type`,
please use [`DeepOmit<Type, Filter>`](../deep-omit/).

Useful in functions which cannot access specified properties

```ts
declare const englishClass: EnglishClass;

const takeSurvey = (englishClass: AnonymousEnglishClass) => {
// Property 'name' does not exist on type '{ yearsOfExperience: number; }'
englishClass.teacher.name;
// ^^^^
// Property 'name' does not exist on type '{ score: number; }'
englishClass.students[0].name;
// ^^^^
};

takeSurvey(englishClass);
```

⚠️ Limitations:

- `StrictDeepOmit` cannot be used when `Type` is generic type
– https://github.com/ts-essentials/ts-essentials/issues/343 (please use `DeepOmit` instead)
- (bug) `StrictDeepOmit` cannot be used on nullable type - https://github.com/ts-essentials/ts-essentials/issues/380
- `StrictDeepOmit` only limits access to specified properties in your codebase, but doesn't remove them in runtime

TS Playground – https://tsplay.dev/wjo7lN
4 changes: 4 additions & 0 deletions lib/strict-deep-omit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { DeepModify } from "../deep-modify";
import { DeepOmit } from "../deep-omit";

export type StrictDeepOmit<Type, Filter extends DeepModify<Type>> = DeepOmit<Type, Filter>;

0 comments on commit 9935d80

Please sign in to comment.