Skip to content

Commit 18a1c04

Browse files
authored
Add RemovePrefix type (#1194)
1 parent 9926e5d commit 18a1c04

File tree

5 files changed

+228
-6
lines changed

5 files changed

+228
-6
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/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/remove-prefix.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {expectType} from 'tsd';
2+
import type {RemovePrefix} from '../source/remove-prefix.d.ts';
3+
4+
expectType<'change'>({} as RemovePrefix<'on-change', 'on-'>);
5+
expectType<'Click'>({} as RemovePrefix<'handleClick', 'handle'>);
6+
expectType<'vscode'>({} as RemovePrefix<'.vscode', '.'>);
7+
expectType<'whitespace'>({} as RemovePrefix<' whitespace', ' '>);
8+
9+
// Prefix not present
10+
expectType<''>({} as RemovePrefix<'', 'foo'>);
11+
expectType<'on-mouse-move'>({} as RemovePrefix<'on-mouse-move', 'click'>);
12+
13+
// Empty prefix
14+
expectType<'baz'>({} as RemovePrefix<'baz', ''>);
15+
16+
// Prefix completely matches the input string
17+
expectType<''>({} as RemovePrefix<'click', 'click'>);
18+
19+
// Prefix partially matches the input string
20+
expectType<'foobar'>({} as RemovePrefix<'foobar', 'foobaz'>);
21+
expectType<'hello'>({} as RemovePrefix<'hello', 'helloworld'>);
22+
23+
// Multiple occurrences of prefix (should only remove from start)
24+
expectType<'bar-bar-foo'>({} as RemovePrefix<'bar-bar-bar-foo', 'bar-'>);
25+
expectType<'foofoo'>({} as RemovePrefix<'foofoofoo', 'foo'>);
26+
27+
// === Non-literals ===
28+
29+
// Input: Non-literal, Prefix: Literal
30+
expectType<string>({} as RemovePrefix<`hover:${string}`, 'hover:'>);
31+
expectType<Capitalize<string>>({} as RemovePrefix<`on${Capitalize<string>}`, 'on'>);
32+
expectType<`${number}`>({} as RemovePrefix<`id-${number}`, 'id-'>);
33+
expectType<`${string}--`>({} as RemovePrefix<`--${string}--`, '--'>);
34+
expectType<`focus:${string}`>({} as RemovePrefix<`hover:focus:${string}`, 'hover:'>);
35+
expectType<`user_${string}`>({} as RemovePrefix<`user_${string}`, 'admin_'>);
36+
expectType<string>({} as RemovePrefix<string, 'on'>);
37+
expectType<`${string}/${number}`>({} as RemovePrefix<`${string}/${number}`, 'foo'>);
38+
39+
// Input: Literal, Prefix: Non-literal
40+
expectType<string>({} as RemovePrefix<'on-click', `${string}-`>);
41+
expectType<string>({} as RemovePrefix<'hover:flex', string>);
42+
expectType<'handle-click'>({} as RemovePrefix<'handle-click', Uppercase<string>>);
43+
expectType<'on-change'>({} as RemovePrefix<'on-change', `${string}--`>);
44+
45+
// Input: Non-literal, Prefix: Non-literal
46+
expectType<string>({} as RemovePrefix<`hover:${string}`, `${string}:`>);
47+
expectType<string>({} as RemovePrefix<`${string}/${number}`, `${string}/`>);
48+
expectType<string>({} as RemovePrefix<string, string>);
49+
expectType<`${string}/${number}`>({} as RemovePrefix<`${string}/${number}`, `${string}:`>);
50+
expectType<`${number}:${number}`>({} as RemovePrefix<`${number}:${number}`, `-${string}`>);
51+
52+
// Unions
53+
expectType<'click' | 'hover' | 'change'>({} as RemovePrefix<'on-click' | 'on-hover' | 'on-change', 'on-'>);
54+
expectType<'click' | 'hover' | 'handle-change'>({} as RemovePrefix<'on-click' | 'on-hover' | 'handle-change', 'on-'>);
55+
expectType<Uppercase<string> | `${number}`>({} as RemovePrefix<`id-${Uppercase<string>}` | `id-${number}`, 'id-'>);
56+
expectType<string>({} as RemovePrefix<`hover:${string}` | `focus:${string}`, `${string}:`>);
57+
58+
expectType<'-change' | 'change'>({} as RemovePrefix<'on-change', 'on' | 'on-'>);
59+
expectType<'change' | 'on-change'>({} as RemovePrefix<'on-change', 'on-' | 'handle-'>);
60+
expectType<'on-change'>({} as RemovePrefix<'on-change', 'off-' | 'handle-'>);
61+
expectType<string>({} as RemovePrefix<'on-change', `${string}-` | 'on'>);
62+
63+
expectType<'on:change' | 'onChange'>({} as RemovePrefix<'on:change' | 'onChange', `${string}-`>);
64+
expectType<'name' | 'get-name' | 'age' | 'set-age' | 'other'>(
65+
{} as RemovePrefix<'get-name' | 'set-age' | 'other', 'get-' | 'set-'>,
66+
);
67+
expectType<string>({} as RemovePrefix<`id:${Uppercase<string>}` | `id/${number}`, `${string}:` | `${string}/`>);
68+
69+
// Boundary types
70+
expectType<any>({} as RemovePrefix<any, 'foo'>);
71+
expectType<never>({} as RemovePrefix<never, 'foo'>);
72+
expectType<string>({} as RemovePrefix<'on-change', any>);
73+
expectType<'on-change'>({} as RemovePrefix<'on-change', never>);
74+
75+
// === strict: false ===
76+
77+
// No effect if `Prefix` is a literal
78+
expectType<'change'>({} as RemovePrefix<'on-change', 'on-', {strict: false}>);
79+
expectType<'change' | 'hover'>({} as RemovePrefix<'on-change' | 'on-hover', 'on-', {strict: false}>);
80+
expectType<Capitalize<string>>({} as RemovePrefix<`handle${Capitalize<string>}`, 'handle', {strict: false}>);
81+
82+
expectType<'click'>({} as RemovePrefix<'on-click', `${string}-`, {strict: false}>);
83+
expectType<'over:flex'>({} as RemovePrefix<'hover:flex', string, {strict: false}>);
84+
expectType<`${number}`>({} as RemovePrefix<`${string}/${number}`, `${string}/`, {strict: false}>);
85+
expectType<'change' | '-change'>({} as RemovePrefix<'on-change', `${string}-` | 'on', {strict: false}>);
86+
expectType<'on:change' | 'change'>({} as RemovePrefix<'on:change' | 'on-change', `${string}-`, {strict: false}>);
87+
expectType<Uppercase<string> | `id:${Uppercase<string>}` | `${number}` | `id/${number}`>(
88+
{} as RemovePrefix<`id:${Uppercase<string>}` | `id/${number}`, `${string}:` | `${string}/`, {strict: false}>,
89+
);
90+
91+
// Generic assignability test
92+
type Assignability<S extends string> = S;
93+
// Output of `RemovePrefix` should be assignable to `string`.
94+
type Test1<S extends string, Prefix extends string> = Assignability<RemovePrefix<S, Prefix>>;
95+
type Test2<S extends Uppercase<string>, Prefix extends '-' | '/' | '#'> = Assignability<RemovePrefix<S, Prefix>>;

0 commit comments

Comments
 (0)