Skip to content

Commit 4d7cc50

Browse files
authored
ConditionalKeys: Fix behavior with arrays and unions (#1198)
1 parent 18a1c04 commit 4d7cc50

File tree

2 files changed

+112
-20
lines changed

2 files changed

+112
-20
lines changed

source/conditional-keys.d.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type {If} from './if.d.ts';
2-
import type {IsNever} from './is-never.d.ts';
1+
import type {ExtendsStrict} from './extends-strict.d.ts';
2+
import type {IfNotAnyOrNever} from './internal/type.d.ts';
3+
import type {TupleToObject} from './tuple-to-object.d.ts';
4+
import type {UnknownArray} from './unknown-array.d.ts';
35

46
/**
57
Extract the keys from a type where the value type of the key extends the given `Condition`.
@@ -10,39 +12,50 @@ Internally this is used for the `ConditionalPick` and `ConditionalExcept` types.
1012
```
1113
import type {ConditionalKeys} from 'type-fest';
1214
13-
interface Example {
15+
type Example = {
1416
a: string;
1517
b: string | number;
1618
c?: string;
1719
d: {};
18-
}
20+
};
1921
2022
type StringKeysOnly = ConditionalKeys<Example, string>;
2123
//=> 'a'
2224
```
2325
24-
To support partial types, make sure your `Condition` is a union of undefined (for example, `string | undefined`) as demonstrated below.
26+
Note: To extract optional keys, make sure your `Condition` is a union of `undefined` (for example, `string | undefined`) as demonstrated below.
2527
2628
@example
2729
```
2830
import type {ConditionalKeys} from 'type-fest';
2931
30-
type StringKeysAndUndefined = ConditionalKeys<Example, string | undefined>;
31-
//=> 'a' | 'c'
32+
type StringKeysAndUndefined = ConditionalKeys<{a?: string}, string | undefined>;
33+
//=> 'a'
34+
35+
type NoMatchingKeys = ConditionalKeys<{a?: string}, string>;
36+
//=> never
37+
```
38+
39+
You can also extract array indices whose value match the specified condition, as shown below:
40+
```
41+
import type {ConditionalKeys} from 'type-fest';
42+
43+
type StringValueIndices = ConditionalKeys<[string, number, string], string>;
44+
//=> '0' | '2'
45+
46+
type NumberValueIndices = ConditionalKeys<[string, number?, string?], number | undefined>;
47+
//=> '1'
3248
```
3349
3450
@category Object
3551
*/
36-
export type ConditionalKeys<Base, Condition> =
37-
{
38-
// Map through all the keys of the given base type.
39-
[Key in keyof Base]-?:
40-
// Pick only keys with types extending the given `Condition` type.
41-
Base[Key] extends Condition
42-
// Retain this key
43-
// If the value for the key extends never, only include it if `Condition` also extends never
44-
? If<IsNever<Base[Key]>, If<IsNever<Condition>, Key, never>, Key>
45-
// Discard this key since the condition fails.
46-
: never;
47-
// Convert the produced object into a union type of the keys which passed the conditional test.
48-
}[keyof Base];
52+
export type ConditionalKeys<Base, Condition> = (Base extends UnknownArray ? TupleToObject<Base> : Base) extends infer _Base // Remove non-numeric keys from arrays
53+
? IfNotAnyOrNever<_Base, _ConditionalKeys<_Base, Condition>, keyof _Base>
54+
: never;
55+
56+
type _ConditionalKeys<Base, Condition> = keyof {
57+
[
58+
Key in (keyof Base & {}) as // `& {}` prevents homomorphism
59+
ExtendsStrict<Base[Key], Condition> extends true ? Key : never
60+
]: never
61+
};

test-d/conditional-keys.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,82 @@ expectType<'a' | 'c'>(exampleConditionalKeysWithUndefined);
1717

1818
declare const exampleConditionalKeysTargetingNever: ConditionalKeys<Example, never>;
1919
expectType<'e'>(exampleConditionalKeysTargetingNever);
20+
21+
// Readonly modifiers
22+
expectType<'a' | 'c'>({} as ConditionalKeys<{readonly a: string; readonly b: number; c: Uppercase<string>}, string>);
23+
expectType<never>({} as ConditionalKeys<{readonly a?: string; readonly b: number}, string>);
24+
expectType<'a'>({} as ConditionalKeys<{readonly a?: string; readonly b?: number}, string | undefined>);
25+
26+
// Optional modifiers
27+
expectType<'b'>({} as ConditionalKeys<{a?: string; b: string}, string>);
28+
expectType<never>({} as ConditionalKeys<{a?: string; b?: string}, string>);
29+
expectType<never>({} as ConditionalKeys<{a?: string; b: string}, undefined>);
30+
expectType<'a' | 'b'>({} as ConditionalKeys<{a?: string; b: string}, string | undefined>);
31+
expectType<'a' | 'b'>({} as ConditionalKeys<{readonly a?: string; readonly b: string}, string | undefined>);
32+
33+
// Union in property values
34+
expectType<never>({} as ConditionalKeys<{a: boolean | number}, boolean>);
35+
expectType<'a' | 'b' | 'c'>({} as ConditionalKeys<{a: boolean | number; b: boolean; c: number}, boolean | number>);
36+
expectType<'b'>({} as ConditionalKeys<{a?: boolean | number; readonly b: boolean | number}, boolean | number>);
37+
expectType<'a' | 'b'>(
38+
{} as ConditionalKeys<{a?: boolean | number; readonly b?: boolean; c: bigint | number}, boolean | number | undefined>,
39+
);
40+
41+
// `never` as condition
42+
expectType<never>({} as ConditionalKeys<{a: string; b: number}, never>);
43+
expectType<never>({} as ConditionalKeys<{readonly a?: string; readonly b?: number}, never>);
44+
expectType<'a' | 'b'>({} as ConditionalKeys<{a: never; b: never}, never>);
45+
expectType<'a' | 'b'>({} as ConditionalKeys<{a: never; b: never}, any>);
46+
expectType<never>({} as ConditionalKeys<{a?: never; b?: never}, never>);
47+
expectType<'a' | 'b'>({} as ConditionalKeys<{a?: never; b?: never}, undefined>);
48+
49+
// Unions
50+
expectType<never>({} as ConditionalKeys<{a: string} | {b: number}, string | number>);
51+
expectType<never>({} as ConditionalKeys<{a: string} | {b: number}, string>);
52+
expectType<never>({} as ConditionalKeys<{a: string} | {b: number}, number>);
53+
expectType<never>({} as ConditionalKeys<{a: string} | {a: number}, number>);
54+
expectType<'a'>({} as ConditionalKeys<{a: string} | {a: number}, string | number>);
55+
expectType<'a' | 'b'>(
56+
{} as ConditionalKeys<{a: string; b: bigint} | {a: number; b: string; c: boolean}, string | number | bigint>,
57+
);
58+
expectType<'a'>({} as ConditionalKeys<{a: string} | {a: Lowercase<string>; b: Uppercase<string>}, string>);
59+
expectType<never>({} as ConditionalKeys<{length: number} | [number], number>);
60+
61+
// `any`/`unknown` as condition
62+
expectType<'a' | 'b' | 'c' | 'd' | 'e'>(
63+
{} as ConditionalKeys<{a?: string; b: string | number; readonly c: boolean; readonly d?: bigint; e: never}, any>,
64+
);
65+
expectType<'a' | 'b' | 'c' | 'd'>(
66+
{} as ConditionalKeys<{a?: string; b: string | number; readonly c: boolean; readonly d?: bigint; e: never}, unknown>,
67+
);
68+
69+
// Index signatures
70+
expectType<string | number>({} as ConditionalKeys<{[x: string]: boolean}, boolean>);
71+
expectType<string | number>({} as ConditionalKeys<{[x: string]: string; a: string}, string>);
72+
expectType<Uppercase<string> | 'a'>({} as ConditionalKeys<{[x: Uppercase<string>]: string; a: string}, string>);
73+
expectType<number | 'a'>({} as ConditionalKeys<{[x: Uppercase<string>]: string; [x: number]: number; a: number}, number>);
74+
75+
// Arrays and tuples
76+
expectType<'0' | '2'>({} as ConditionalKeys<[string, number, string], string>);
77+
expectType<'0' | '1'>({} as ConditionalKeys<[string, string, string | number], string>);
78+
expectType<'0' | '1' | '2'>({} as ConditionalKeys<[string, number, string], any>);
79+
expectType<'0'>({} as ConditionalKeys<[string, string?, string?], string>);
80+
expectType<'0' | '1' | '2'>({} as ConditionalKeys<[string, string?, string?], string | undefined>);
81+
expectType<never>({} as ConditionalKeys<[string, number, ...boolean[]], boolean>);
82+
expectType<'0'>({} as ConditionalKeys<[string, number?, ...boolean[]], string | number | boolean>);
83+
expectType<'1'>({} as ConditionalKeys<[string?, number?, ...boolean[]], number | undefined>);
84+
expectType<'0' | '1' | number>({} as ConditionalKeys<[string, number, ...boolean[]], string | number | boolean>);
85+
expectType<'0'>({} as ConditionalKeys<[bigint, ...number[], bigint], bigint>);
86+
expectType<never>({} as ConditionalKeys<[...boolean[], string, string], string>);
87+
expectType<number>({} as ConditionalKeys<[...string[], string, string], string>);
88+
expectType<number>({} as ConditionalKeys<string[], string>);
89+
expectType<never>({} as ConditionalKeys<string[], number>);
90+
91+
// Primitives
92+
expectType<'length'>({} as ConditionalKeys<string, number>);
93+
expectType<'valueOf'>({} as ConditionalKeys<number, () => number>);
94+
95+
// Boundary cases
96+
expectType<never>({} as ConditionalKeys<{}, boolean>);
97+
expectType<PropertyKey>({} as ConditionalKeys<any, boolean>);
98+
expectType<never>({} as ConditionalKeys<never, boolean>);

0 commit comments

Comments
 (0)