Skip to content

Commit a240e8d

Browse files
authored
Merge branch 'main' into numeric-like/union-min
2 parents ca7eca2 + 18a1c04 commit a240e8d

File tree

7 files changed

+330
-27
lines changed

7 files changed

+330
-27
lines changed

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export type {Get} from './source/get.d.ts';
181181
export type {LastArrayElement} from './source/last-array-element.d.ts';
182182
export type {ConditionalSimplify} from './source/conditional-simplify.d.ts';
183183
export type {ConditionalSimplifyDeep} from './source/conditional-simplify-deep.d.ts';
184+
export type {RemovePrefix} from './source/remove-prefix.d.ts';
184185

185186
// Miscellaneous
186187
export type {GlobalThis} from './source/global-this.d.ts';

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ Click the type names for complete docs.
233233
- [`Replace`](source/replace.d.ts) - Represents a string with some or all matches replaced by a replacement.
234234
- [`StringSlice`](source/string-slice.d.ts) - Returns a string slice of a given range, just like `String#slice()`.
235235
- [`StringRepeat`](source/string-repeat.d.ts) - Returns a new string which contains the specified number of copies of a given string, just like `String#repeat()`.
236+
- [`RemovePrefix`](source/remove-prefix.d.ts) - Removes the specified prefix from the start of a string.
236237

237238
### Array
238239

source/delimiter-case.d.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {ApplyDefaultOptions, AsciiPunctuation, StartsWith} from './internal/index.d.ts';
22
import type {IsStringLiteral} from './is-literal.d.ts';
33
import type {Merge} from './merge.d.ts';
4+
import type {RemovePrefix} from './remove-prefix.d.ts';
45
import type {DefaultWordsOptions, Words, WordsOptions} from './words.d.ts';
56

67
export type DefaultDelimiterCaseOptions = Merge<DefaultWordsOptions, {splitOnNumbers: false}>;
@@ -21,10 +22,6 @@ type DelimiterCaseFromArray<
2122
}${FirstWord}`>
2223
: OutputString;
2324

24-
type RemoveFirstLetter<S extends string> = S extends `${infer _}${infer Rest}`
25-
? Rest
26-
: '';
27-
2825
/**
2926
Convert a string literal to a custom string delimiter casing.
3027
@@ -71,8 +68,8 @@ export type DelimiterCase<
7168
> = Value extends string
7269
? IsStringLiteral<Value> extends false
7370
? Value
74-
: Lowercase<RemoveFirstLetter<DelimiterCaseFromArray<
71+
: Lowercase<RemovePrefix<DelimiterCaseFromArray<
7572
Words<Value, ApplyDefaultOptions<WordsOptions, DefaultDelimiterCaseOptions, Options>>,
7673
Delimiter
77-
>>>
74+
>, string, {strict: false}>>
7875
: Value;

source/paths.d.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions} from './internal/index.d.ts';
2-
import type {EmptyObject} from './empty-object.d.ts';
32
import type {IsAny} from './is-any.d.ts';
43
import type {UnknownArray} from './unknown-array.d.ts';
54
import type {Subtract} from './subtract.d.ts';
65
import type {GreaterThan} from './greater-than.d.ts';
6+
import type {IsNever} from './is-never.d.ts';
77

88
/**
99
Paths options.
@@ -195,28 +195,34 @@ type _Paths<T, Options extends Required<PathsOptions>> =
195195
type InternalPaths<T, Options extends Required<PathsOptions>> =
196196
Options['maxRecursionDepth'] extends infer MaxDepth extends number
197197
? Required<T> extends infer T
198-
? T extends EmptyObject | readonly []
198+
? T extends readonly []
199199
? never
200-
: {
201-
[Key in keyof T]:
202-
Key extends string | number // Limit `Key` to string or number.
203-
? (
204-
Options['bracketNotation'] extends true
205-
? IsNumberLike<Key> extends true
206-
? `[${Key}]`
207-
: (Key | ToString<Key>)
208-
: Options['bracketNotation'] extends false
209-
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
210-
? (Key | ToString<Key>)
211-
: never
212-
) extends infer TranformedKey extends string | number ?
213-
// 1. If style is 'a[0].b' and 'Key' is a numberlike value like 3 or '3', transform 'Key' to `[${Key}]`, else to `${Key}` | Key
214-
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
200+
: IsNever<keyof T> extends true // Check for empty object
201+
? never
202+
: {
203+
[Key in keyof T]:
204+
Key extends string | number // Limit `Key` to string or number.
205+
? (
206+
Options['bracketNotation'] extends true
207+
? IsNumberLike<Key> extends true
208+
? `[${Key}]`
209+
: (Key | ToString<Key>)
210+
: Options['bracketNotation'] extends false
211+
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
212+
? (Key | ToString<Key>)
213+
: never
214+
) extends infer TranformedKey extends string | number ?
215+
// 1. If style is 'a[0].b' and 'Key' is a numberlike value like 3 or '3', transform 'Key' to `[${Key}]`, else to `${Key}` | Key
216+
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
215217
| ((Options['leavesOnly'] extends true
216218
? MaxDepth extends 0
217219
? TranformedKey
218-
: T[Key] extends EmptyObject | readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
219-
? TranformedKey
220+
: T[Key] extends infer Value
221+
? (Value extends readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
222+
? TranformedKey
223+
: IsNever<keyof Value> extends true // Check for empty object
224+
? TranformedKey
225+
: never)
220226
: never
221227
: TranformedKey
222228
) extends infer _TransformedKey
@@ -252,8 +258,8 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
252258
: never
253259
: never
254260
)
261+
: never
255262
: never
256-
: never
257-
}[keyof T & (T extends UnknownArray ? number : unknown)]
263+
}[keyof T & (T extends UnknownArray ? number : unknown)]
258264
: never
259265
: never;

source/remove-prefix.d.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type {ApplyDefaultOptions} from './internal/object.d.ts';
2+
import type {IfNotAnyOrNever, Not} from './internal/type.d.ts';
3+
import type {IsStringLiteral} from './is-literal.d.ts';
4+
import type {Or} from './or.d.ts';
5+
6+
/**
7+
@see {@link RemovePrefix}
8+
*/
9+
type RemovePrefixOptions = {
10+
/**
11+
When enabled, instantiations with non-literal prefixes (e.g., `string`, `Uppercase<string>`, `` `on${string}` ``) simply return `string`, since their precise structure cannot be statically determined.
12+
13+
Note: Disabling this option can produce misleading results that might not reflect the actual runtime behavior.
14+
For example, ``RemovePrefix<'on-change', `${string}-`, {strict: false}>`` returns `'change'`, but at runtime, prefix could be `'handle-'` (which satisfies `` `${string}-` ``) and removing `'handle-'` from `'on-change'` would not result in `'change'`.
15+
16+
So, it is recommended to not disable this option unless you are aware of the implications.
17+
18+
@default true
19+
20+
@example
21+
```
22+
type A = RemovePrefix<'on-change', `${string}-`, {strict: true}>;
23+
//=> string
24+
25+
type B = RemovePrefix<'on-change', `${string}-`, {strict: false}>;
26+
//=> 'change'
27+
28+
type C = RemovePrefix<'on-change', string, {strict: true}>;
29+
//=> string
30+
31+
type D = RemovePrefix<'on-change', string, {strict: false}>;
32+
//=> 'n-change'
33+
34+
type E = RemovePrefix<`${string}/${number}`, `${string}/`, {strict: true}>;
35+
//=> string
36+
37+
type F = RemovePrefix<`${string}/${number}`, `${string}/`, {strict: false}>;
38+
//=> `${number}`
39+
```
40+
41+
Note: This option has no effect when only the input string type is non-literal. For example, ``RemovePrefix<`on-${string}`, 'on-'>`` will always return `string`.
42+
43+
@example
44+
```
45+
import type {RemovePrefix} from 'type-fest';
46+
47+
type A = RemovePrefix<`on-${string}`, 'on-', {strict: true}>;
48+
//=> string
49+
50+
type B = RemovePrefix<`on-${string}`, 'on-', {strict: false}>;
51+
//=> string
52+
53+
type C = RemovePrefix<`id-${number}`, 'id-', {strict: true}>;
54+
//=> `${number}`
55+
56+
type D = RemovePrefix<`id-${number}`, 'id-', {strict: false}>;
57+
//=> `${number}`
58+
```
59+
60+
Note: If it can be statically determined that the input string can never start with the specified non-literal prefix, then the input string is returned as-is, regardless of the value of this option.
61+
For example, ``RemovePrefix<`${string}/${number}`, `${string}:`>`` returns `` `${string}/${number}` ``, since a string of type `` `${string}/${number}` `` can never start with a prefix of type `` `${string}:` ``.
62+
```
63+
import type {RemovePrefix} from 'type-fest';
64+
65+
type A = RemovePrefix<`${string}/${number}`, `${string}:`, {strict: true}>;
66+
//=> `${string}/${number}`
67+
68+
type B = RemovePrefix<`${string}/${number}`, `${string}:`, {strict: false}>;
69+
//=> `${string}/${number}`
70+
71+
type C = RemovePrefix<'on-change', `${number}-`, {strict: true}>;
72+
//=> 'on-change'
73+
74+
type D = RemovePrefix<'on-change', `${number}-`, {strict: false}>;
75+
//=> 'on-change'
76+
```
77+
*/
78+
strict?: boolean;
79+
};
80+
81+
type DefaultRemovePrefixOptions = {
82+
strict: true;
83+
};
84+
85+
/**
86+
Removes the specified prefix from the start of a string.
87+
88+
@example
89+
```
90+
import type {RemovePrefix} from 'type-fest';
91+
92+
type A = RemovePrefix<'on-change', 'on-'>;
93+
//=> 'change'
94+
95+
type B = RemovePrefix<'sm:flex' | 'sm:p-4' | 'sm:gap-2', 'sm:'>;
96+
//=> 'flex' | 'p-4' | 'gap-2'
97+
98+
type C = RemovePrefix<'on-change', 'off-'>;
99+
//=> 'on-change'
100+
101+
type D = RemovePrefix<`handle${Capitalize<string>}`, 'handle'>;
102+
//=> Capitalize<string>
103+
```
104+
105+
@see {@link RemovePrefixOptions}
106+
107+
@category String
108+
@category Template literal
109+
*/
110+
export type RemovePrefix<S extends string, Prefix extends string, Options extends RemovePrefixOptions = {}> =
111+
IfNotAnyOrNever<
112+
S,
113+
IfNotAnyOrNever<
114+
Prefix,
115+
_RemovePrefix<S, Prefix, ApplyDefaultOptions<RemovePrefixOptions, DefaultRemovePrefixOptions, Options>>,
116+
string,
117+
S
118+
>
119+
>;
120+
121+
type _RemovePrefix<S extends string, Prefix extends string, Options extends Required<RemovePrefixOptions>> =
122+
Prefix extends string // For distributing `Prefix`
123+
? S extends `${Prefix}${infer Rest}`
124+
? Or<IsStringLiteral<Prefix>, Not<Options['strict']>> extends true
125+
? Rest
126+
: string // Fallback to `string` when `Prefix` is non-literal and `strict` is disabled
127+
: S // Return back `S` when `Prefix` is not present at the start of `S`
128+
: never;

test-d/paths.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ expectType<'a.b' | 'a.c' | 'a.d' | 'a.e' | 'a.f.g' | 'h'>(leaves);
155155
declare const unionLeaves: Paths<{a: {b?: number}} | {a: string; b?: {c: string}}, {leavesOnly: true}>;
156156
expectType<'a.b' | 'a' | 'b.c'>(unionLeaves);
157157

158+
declare const unionLeaves1: Paths<{a: {} | {c: number}}, {leavesOnly: true}>;
159+
expectType<'a' | 'a.c'>(unionLeaves1);
160+
161+
declare const unionLeaves2: Paths<{a: {[x: string]: number} | {c: number}}, {leavesOnly: true}>;
162+
expectType<`a.${string}`>(unionLeaves2); // Collapsed union
163+
158164
declare const emptyObjectLeaves: Paths<{a: {}}, {leavesOnly: true}>;
159165
expectType<'a'>(emptyObjectLeaves);
160166

@@ -387,3 +393,72 @@ expectType<never>(neverDepth);
387393

388394
declare const anyDepth: Paths<DeepObject, {depth: any}>;
389395
expectType<'a' | 'a.b.c' | `a.b2.${number}` | 'a.b3' | 'a.b' | 'a.b2' | 'a.b.c.d'>(anyDepth);
396+
397+
// Index signatures
398+
declare const indexSignature: Paths<{[x: string]: {a: string; b: number}}>;
399+
expectType<string>(indexSignature); // Collapsed union
400+
401+
declare const indexSignature1: Paths<{[x: Lowercase<string>]: {a: string; b: number}}>;
402+
expectType<Lowercase<string> | `${Lowercase<string>}.a` | `${Lowercase<string>}.b`>(indexSignature1);
403+
404+
declare const indexSignature2: Paths<{[x: number]: {0: string; 1: number}}>;
405+
expectType<number | `${number}` | `${number}.0` | `${number}.1`>(indexSignature2);
406+
407+
declare const indexSignature3: Paths<{[x: Uppercase<string>]: {a: string; b: number}}>;
408+
expectType<Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignature3);
409+
410+
declare const indexSignature4: Paths<{a: {[x: symbol]: {b: number; c: number}}}>;
411+
expectType<'a'>(indexSignature4);
412+
413+
declare const indexSignatureWithStaticKeys: Paths<{[x: Uppercase<string>]: {a: string; b: number}; c: number}>;
414+
expectType<'c' | Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignatureWithStaticKeys);
415+
416+
declare const indexSignatureWithStaticKeys1: Paths<{[x: Uppercase<string>]: {a: string; b?: number}; C: {a: 'a'}}>;
417+
expectType<Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignatureWithStaticKeys1); // Collapsed union
418+
419+
declare const nonRootIndexSignature: Paths<{a: {[x: string]: {b: string; c: number}}}>;
420+
expectType<'a' | `a.${string}`>(nonRootIndexSignature); // Collapsed union
421+
422+
declare const nonRootIndexSignature1: Paths<{a: {[x: Lowercase<string>]: {b: string; c: number}}}>;
423+
expectType<'a' | `a.${Lowercase<string>}` | `a.${Lowercase<string>}.b` | `a.${Lowercase<string>}.c`>(nonRootIndexSignature1);
424+
425+
declare const nestedIndexSignature: Paths<{[x: string]: {[x: Lowercase<string>]: {a: string; b: number}}}>;
426+
expectType<string>(nestedIndexSignature);
427+
428+
declare const nestedIndexSignature1: Paths<{[x: Uppercase<string>]: {[x: Lowercase<string>]: {a: string; b: number}}}>;
429+
expectType<Uppercase<string> | `${Uppercase<string>}.${Lowercase<string>}` | `${Uppercase<string>}.${Lowercase<string>}.a` | `${Uppercase<string>}.${Lowercase<string>}.b`>(
430+
nestedIndexSignature1,
431+
);
432+
433+
declare const indexSignatureUnion: Paths<{a: {[x: string]: number} | {b: number}}>;
434+
expectType<'a' | `a.${string}`>(indexSignatureUnion); // Collapsed union
435+
436+
declare const indexSignatureUnion1: Paths<{a: {[x: Uppercase<string>]: number} | {b: number}}>;
437+
expectType<'a' | 'a.b' | `a.${Uppercase<string>}`>(indexSignatureUnion1);
438+
439+
declare const indexSignatureLeaves: Paths<{[x: string]: {a: string; b: number}}, {leavesOnly: true}>;
440+
expectType<`${string}.a` | `${string}.b`>(indexSignatureLeaves);
441+
442+
declare const indexSignatureLeaves1: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {leavesOnly: true}>;
443+
expectType<`a.${string}.b` | `a.${string}.c` | 'd' | 'e.f'>(indexSignatureLeaves1);
444+
445+
declare const indexSignatureLeaves2: Paths<{a: {[x: string]: [] | {b: number}}}, {leavesOnly: true}>;
446+
expectType<`a.${string}`>(indexSignatureLeaves2); // Collapsed union
447+
448+
declare const indexSignatureDepth: Paths<{[x: string]: {a: string; b: number}}, {depth: 1}>;
449+
expectType<`${string}.b` | `${string}.a`>(indexSignatureDepth);
450+
451+
declare const indexSignatureDepth1: Paths<{[x: string]: {a: string; b: number}}, {depth: 0}>;
452+
expectType<string>(indexSignatureDepth1);
453+
454+
declare const indexSignatureDepth2: Paths<{[x: string]: {a: string; b: number}}, {depth: 0 | 1}>;
455+
expectType<string>(indexSignatureDepth2); // Collapsed union
456+
457+
declare const indexSignatureDepth3: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {depth: 0 | 2}>;
458+
expectType<'a' | `a.${string}.b` | `a.${string}.c` | 'd' | 'e'>(indexSignatureDepth3);
459+
460+
declare const indexSignatureDepth4: Paths<{a: {[x: string]: [] | {b: number}}}, {depth: 2}>;
461+
expectType<`a.${string}.b`>(indexSignatureDepth4);
462+
463+
declare const indexSignatureDepthLeaves: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {depth: 0 | 2; leavesOnly: true}>;
464+
expectType<`a.${string}.b` | `a.${string}.c` | 'd'>(indexSignatureDepthLeaves);

0 commit comments

Comments
 (0)