Skip to content

Commit

Permalink
PartialOnUndefinedDeep: Fix it incorrectly removing non-optional pr…
Browse files Browse the repository at this point in the history
…operties when the input type contains an index signature (#764)

Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
clemclx and sindresorhus committed Feb 22, 2024
1 parent e02f228 commit 2f4b55a
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 4 deletions.
6 changes: 6 additions & 0 deletions source/internal.d.ts
Expand Up @@ -2,6 +2,7 @@ import type {Primitive} from './primitive';
import type {Simplify} from './simplify';
import type {Trim} from './trim';
import type {IsAny} from './is-any';
import type {IsLiteral} from './is-literal';
import type {UnknownRecord} from './unknown-record';
import type {IsNever} from './is-never';
import type {UnknownArray} from './unknown-array';
Expand Down Expand Up @@ -357,6 +358,11 @@ IsPrimitive<Object>
*/
export type IsPrimitive<T> = [T] extends [Primitive] ? true : false;

/**
Utility type to retrieve only literal keys from type.
*/
export type LiteralKeyOf<T> = keyof {[K in keyof T as IsLiteral<K> extends true ? K : never]-?: never};

/**
Returns the static, fixed-length portion of the given array, excluding variable-length parts.
Expand Down
7 changes: 4 additions & 3 deletions source/partial-on-undefined-deep.d.ts
@@ -1,4 +1,5 @@
import type {BuiltIns} from './internal';
import type {IfUnknown} from './if-unknown';
import type {BuiltIns, LiteralKeyOf} from './internal';
import type {Merge} from './merge';

/**
Expand Down Expand Up @@ -47,8 +48,8 @@ const testSettings: PartialOnUndefinedDeep<Settings> = {
@category Object
*/
export type PartialOnUndefinedDeep<T, Options extends PartialOnUndefinedDeepOptions = {}> = T extends Record<any, any> | undefined
? {[KeyType in keyof T as undefined extends T[KeyType] ? KeyType : never]?: PartialOnUndefinedDeepValue<T[KeyType], Options>} extends infer U // Make a partial type with all value types accepting undefined (and set them optional)
? Merge<{[KeyType in keyof T as KeyType extends keyof U ? never : KeyType]: PartialOnUndefinedDeepValue<T[KeyType], Options>}, U> // Join all remaining keys not treated in U
? {[KeyType in keyof T as undefined extends T[KeyType] ? IfUnknown<T[KeyType], never, KeyType> : never]?: PartialOnUndefinedDeepValue<T[KeyType], Options>} extends infer U // Make a partial type with all value types accepting undefined (and set them optional)
? Merge<{[KeyType in keyof T as KeyType extends LiteralKeyOf<U> ? never : KeyType]: PartialOnUndefinedDeepValue<T[KeyType], Options>}, U> // Join all remaining keys not treated in U
: never // Should not happen
: T;

Expand Down
17 changes: 16 additions & 1 deletion test-d/partial-on-undefined-deep.ts
@@ -1,4 +1,4 @@
import {expectAssignable} from 'tsd';
import {expectAssignable, expectType} from 'tsd';
import type {PartialOnUndefinedDeep} from '../index';

type TestingType = {
Expand All @@ -22,8 +22,11 @@ type TestingType = {
array2: Array<{propertyA: string; propertyB: number | undefined}> | undefined;
readonly1: readonly any[] | undefined;
readonly2: ReadonlyArray<{propertyA: string; propertyB: number | undefined}> | undefined;
readonly readonlyProperty: string | undefined;
tuple: ['test1', {propertyA: string; propertyB: number | undefined}] | undefined;
};
declare const indexType: {[k: string]: string | undefined; propertyA: string; propertyB: string | undefined};
declare const indexTypeUnknown: {[k: string]: unknown; propertyA: string; propertyB: number | undefined};

// Default behavior, without recursion into arrays/tuples
declare const foo: PartialOnUndefinedDeep<TestingType>;
Expand All @@ -48,9 +51,15 @@ expectAssignable<{
array2?: TestingType['array2'];
readonly1?: TestingType['readonly1'];
readonly2?: TestingType['readonly2'];
readonly readonlyProperty?: TestingType['readonlyProperty'];
tuple?: TestingType['tuple'];
}>(foo);

declare const indexTypeWithoutRecursion: PartialOnUndefinedDeep<typeof indexType>;
declare const indexTypeUnknownWithoutRecursion: PartialOnUndefinedDeep<typeof indexTypeUnknown>;
expectType<{[k: string]: string | undefined; propertyA: string; propertyB?: string | undefined}>(indexTypeWithoutRecursion);
expectType<{[k: string]: unknown; propertyA: string; propertyB?: number | undefined}>(indexTypeUnknownWithoutRecursion);

// With recursion into arrays/tuples activated
declare const bar: PartialOnUndefinedDeep<TestingType, {recurseIntoArrays: true}>;
expectAssignable<{
Expand All @@ -74,5 +83,11 @@ expectAssignable<{
array2?: Array<{propertyA: string; propertyB?: number | undefined}> | undefined;
readonly1?: TestingType['readonly1'];
readonly2?: ReadonlyArray<{propertyA: string; propertyB?: number | undefined}> | undefined;
readonly readonlyProperty?: TestingType['readonlyProperty'];
tuple?: ['test1', {propertyA: string; propertyB?: number | undefined}] | undefined;
}>(bar);

declare const indexTypeWithRecursion: PartialOnUndefinedDeep<typeof indexType, {recurseIntoArrays: true}>;
declare const indexTypeUnknownWithRecursion: PartialOnUndefinedDeep<typeof indexTypeUnknown, {recurseIntoArrays: true}>;
expectType<{[k: string]: string | undefined; propertyA: string; propertyB?: string | undefined}>(indexTypeWithRecursion);
expectType<{[k: string]: unknown; propertyA: string; propertyB?: number | undefined}>(indexTypeUnknownWithRecursion);

0 comments on commit 2f4b55a

Please sign in to comment.