Skip to content

Commit 236fae0

Browse files
authoredNov 14, 2022
feat: better type supports (#20)
1 parent 3ba367a commit 236fae0

File tree

4 files changed

+188
-28
lines changed

4 files changed

+188
-28
lines changed
 

‎.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
dist
2+
package.json

‎src/index.ts

+50-28
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CamelCase, JoinByCase, PascalCase, SplitByCase } from './types'
2+
13
const NUMBER_CHAR_RE = /[0-9]/
24

35
export function isUppercase (char: string = ''): boolean | null {
@@ -7,13 +9,17 @@ export function isUppercase (char: string = ''): boolean | null {
79
return char.toUpperCase() === char
810
}
911

10-
const STR_SPLITTERS = ['-', '_', '/', '.']
12+
const STR_SPLITTERS = ['-', '_', '/', '.'] as const
1113

12-
export function splitByCase (str: string, splitters = STR_SPLITTERS): string[] {
14+
/* eslint-disable no-redeclare */
15+
export function splitByCase <T extends string> (str: T): SplitByCase<T>
16+
export function splitByCase <T extends string, Sep extends readonly string[]> (str: T, separators: Sep): SplitByCase<T, Sep[number]>
17+
export function splitByCase <T extends string, Sep extends readonly string[]> (str: T, separators?: Sep) {
18+
const splitters = separators ?? STR_SPLITTERS
1319
const parts: string[] = []
1420

1521
if (!str || typeof str !== 'string') {
16-
return parts
22+
return parts as SplitByCase<T, Sep[number]>
1723
}
1824

1925
let buff: string = ''
@@ -23,7 +29,7 @@ export function splitByCase (str: string, splitters = STR_SPLITTERS): string[] {
2329

2430
for (const char of str.split('')) {
2531
// Splitter
26-
const isSplitter = splitters.includes(char)
32+
const isSplitter = (splitters as unknown as string).includes(char)
2733
if (isSplitter === true) {
2834
parts.push(buff)
2935
buff = ''
@@ -58,39 +64,55 @@ export function splitByCase (str: string, splitters = STR_SPLITTERS): string[] {
5864

5965
parts.push(buff)
6066

61-
return parts
67+
return parts as SplitByCase<T, Sep[number]>
6268
}
69+
/* eslint-enable no-redeclare */
6370

64-
export function upperFirst (str: string): string {
65-
if (!str) {
66-
return ''
67-
}
68-
return str[0].toUpperCase() + str.substring(1)
71+
export function upperFirst <S extends string> (str: S): Capitalize<S> {
72+
return (!str ? '' : str[0].toUpperCase() + str.substring(1)) as Capitalize<S>
6973
}
7074

71-
export function lowerFirst (str: string): string {
72-
if (!str) {
73-
return ''
74-
}
75-
return str[0].toLowerCase() + str.substring(1)
75+
export function lowerFirst <S extends string> (str: S): Uncapitalize<S> {
76+
return (!str ? '' : str[0].toLowerCase() + str.substring(1)) as Uncapitalize<S>
7677
}
7778

78-
export function pascalCase (str: string | string[] = ''): string {
79-
return (Array.isArray(str) ? str : splitByCase(str))
80-
.map(p => upperFirst(p))
81-
.join('')
79+
/* eslint-disable no-redeclare */
80+
export function pascalCase (): ''
81+
export function pascalCase <T extends string | readonly string[]> (str: T): PascalCase<T>
82+
export function pascalCase <T extends string | readonly string[]> (str?: T) {
83+
return !str
84+
? ''
85+
: (Array.isArray(str) ? str : splitByCase(str as string))
86+
.map(p => upperFirst(p))
87+
.join('') as PascalCase<T>
8288
}
89+
/* eslint-enable no-redeclare */
8390

84-
export function camelCase (str: string | string[] = ''): string {
85-
return lowerFirst(pascalCase(str))
91+
/* eslint-disable no-redeclare */
92+
export function camelCase (): ''
93+
export function camelCase <T extends string | readonly string[]> (str: T): CamelCase<T>
94+
export function camelCase <T extends string | readonly string[]> (str?: T) {
95+
return lowerFirst(pascalCase(str)) as CamelCase<T>
8696
}
87-
88-
export function kebabCase (str: string | string[] = '', joiner = '-'): string {
89-
return (Array.isArray(str) ? str : splitByCase(str))
90-
.map((p = '') => p.toLowerCase())
91-
.join(joiner)
97+
/* eslint-enable no-redeclare */
98+
99+
/* eslint-disable no-redeclare */
100+
export function kebabCase (): ''
101+
export function kebabCase <T extends string | readonly string[]> (str: T): JoinByCase<T, '-'>
102+
export function kebabCase <T extends string | readonly string[], Joiner extends string> (str: T, joiner: Joiner): JoinByCase<T, Joiner>
103+
export function kebabCase <T extends string | readonly string[], Joiner extends string> (str?: T, joiner?: Joiner) {
104+
return !str
105+
? ''
106+
: (Array.isArray(str) ? str : splitByCase(str as string))
107+
.map(p => p.toLowerCase())
108+
.join(joiner ?? '-') as JoinByCase<T, Joiner>
92109
}
110+
/* eslint-enable no-redeclare */
93111

94-
export function snakeCase (str: string | string[] = '') {
95-
return kebabCase(str, '_')
112+
/* eslint-disable no-redeclare */
113+
export function snakeCase (): ''
114+
export function snakeCase <T extends string | readonly string[]> (str: T): JoinByCase<T, '_'>
115+
export function snakeCase <T extends string | readonly string[]> (str?: T) {
116+
return kebabCase(str, '_') as JoinByCase<T, '_'>
96117
}
118+
/* eslint-enable no-redeclare */

‎src/types.ts

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
type Assert<T extends true> = T
2+
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false
3+
4+
type Splitter = '-' | '_' | '/' | '.'
5+
type FirstOfString<S extends string> = S extends `${infer F}${string}` ? F : never
6+
type RemoveFirstOfString<S extends string> = S extends `${string}${infer R}` ? R : never
7+
type IsUpper<S extends string> = S extends Uppercase<S> ? true : false
8+
type IsLower<S extends string> = S extends Lowercase<S> ? true : false
9+
type SameLetterCase<X extends string, Y extends string> = IsUpper<X> extends IsUpper<Y> ? true : IsLower<X> extends IsLower<Y> ? true : false
10+
type CapitalizedWords<T extends readonly string[], Acc extends string = ''> =
11+
T extends readonly [infer F extends string, ...infer R extends string[]]
12+
? CapitalizedWords<R, `${Acc}${Capitalize<F>}`>
13+
: Acc
14+
type JoinLowercaseWords<T extends readonly string[], Joiner extends string, Acc extends string = ''> =
15+
T extends readonly [infer F extends string, ...infer R extends string[]]
16+
? Acc extends ''
17+
? JoinLowercaseWords<R, Joiner, `${Acc}${Lowercase<F>}`>
18+
: JoinLowercaseWords<R, Joiner, `${Acc}${Joiner}${Lowercase<F>}`>
19+
: Acc
20+
21+
type LastOfArray<T extends any[]> = T extends [...any, infer R] ? R : never
22+
type RemoveLastOfArray<T extends any[]> = T extends [...infer F, any] ? F : never
23+
24+
export type SplitByCase<T, Sep extends string = Splitter, Acc extends unknown[] = []> =
25+
string extends Sep
26+
? string[]
27+
: T extends `${infer F}${infer R}`
28+
? [LastOfArray<Acc>] extends [never]
29+
? SplitByCase<R, Sep, [F]>
30+
: LastOfArray<Acc> extends string
31+
? R extends ''
32+
? SplitByCase<R, Sep, [...RemoveLastOfArray<Acc>, `${LastOfArray<Acc>}${F}`]>
33+
: SameLetterCase<F, FirstOfString<R>> extends true
34+
? F extends Sep
35+
? FirstOfString<R> extends Sep
36+
? SplitByCase<R, Sep, [...Acc, '']>
37+
: IsUpper<FirstOfString<R>> extends true
38+
? SplitByCase<RemoveFirstOfString<R>, Sep, [...Acc, FirstOfString<R>]>
39+
: SplitByCase<R, Sep, [...Acc, '']>
40+
: SplitByCase<R, Sep, [...RemoveLastOfArray<Acc>, `${LastOfArray<Acc>}${F}`]>
41+
: IsLower<F> extends true
42+
? SplitByCase<RemoveFirstOfString<R>, Sep, [...RemoveLastOfArray<Acc>, `${LastOfArray<Acc>}${F}`, FirstOfString<R>]>
43+
: SplitByCase<R, Sep, [...Acc, F]>
44+
: never
45+
: Acc extends []
46+
? T extends '' ? [] : string[]
47+
: Acc
48+
49+
export type PascalCase<T> =
50+
string extends T
51+
? string
52+
: string[] extends T
53+
? string
54+
: T extends string
55+
? SplitByCase<T> extends readonly string[]
56+
? CapitalizedWords<SplitByCase<T>>
57+
: never
58+
: T extends readonly string[]
59+
? CapitalizedWords<T>
60+
: never
61+
62+
export type CamelCase<T> =
63+
string extends T
64+
? string
65+
: string[] extends T
66+
? string
67+
: Uncapitalize<PascalCase<T>>
68+
69+
export type JoinByCase<T, Joiner extends string> =
70+
string extends T
71+
? string
72+
: string[] extends T
73+
? string
74+
: T extends string
75+
? SplitByCase<T> extends readonly string[]
76+
? JoinLowercaseWords<SplitByCase<T>, Joiner>
77+
: never
78+
: T extends readonly string[]
79+
? JoinLowercaseWords<T, Joiner>
80+
: never
81+
82+
/* eslint-disable @typescript-eslint/no-unused-vars */
83+
type tests = [
84+
// SplitByCase
85+
Assert<Equal<SplitByCase<string>, string[]>>,
86+
// default splitters
87+
Assert<Equal<SplitByCase<''>, []>>,
88+
Assert<Equal<SplitByCase<'foo'>, ['foo']>>,
89+
Assert<Equal<SplitByCase<'foo_bar-baz/qux'>, ['foo', 'bar', 'baz', 'qux']>>,
90+
Assert<Equal<SplitByCase<'foo--bar-Baz'>, ['foo', '', 'bar', 'Baz']>>,
91+
Assert<Equal<SplitByCase<'foo123-bar'>, ['foo123', 'bar']>>,
92+
Assert<Equal<SplitByCase<'fooBar'>, ['foo', 'Bar']>>,
93+
Assert<Equal<SplitByCase<'fooBARBaz'>, ['foo', 'BAR', 'Baz']>>,
94+
Assert<Equal<SplitByCase<'FOOBar'>, ['FOO', 'Bar']>>,
95+
Assert<Equal<SplitByCase<'ALink'>, ['A', 'Link']>>,
96+
// custom splitters
97+
Assert<Equal<SplitByCase<'foo\\Bar.fuzz-FIZz', '\\' | '.' | '-'>, ['foo', 'Bar', 'fuzz', 'FI', 'Zz']>>,
98+
99+
// PascalCase
100+
Assert<Equal<PascalCase<string>, string>>,
101+
Assert<Equal<PascalCase<string[]>, string>>,
102+
// string
103+
Assert<Equal<PascalCase<''>, ''>>,
104+
Assert<Equal<PascalCase<'foo'>, 'Foo'>>,
105+
Assert<Equal<PascalCase<'foo-bAr'>, 'FooBAr'>>,
106+
Assert<Equal<PascalCase<'FooBARb'>, 'FooBARb'>>,
107+
Assert<Equal<PascalCase<'foo_bar-baz/qux'>, 'FooBarBazQux'>>,
108+
Assert<Equal<PascalCase<'foo--bar-Baz'>, 'FooBarBaz'>>,
109+
// array
110+
Assert<Equal<PascalCase<['foo', 'Bar']>, 'FooBar'>>,
111+
Assert<Equal<PascalCase<['foo', 'Bar', 'fuzz', 'FI', 'Zz']>, 'FooBarFuzzFIZz'>>,
112+
113+
// CamelCase
114+
Assert<Equal<CamelCase<string>, string>>,
115+
Assert<Equal<CamelCase<string[]>, string>>,
116+
// string
117+
Assert<Equal<CamelCase<''>, ''>>,
118+
Assert<Equal<CamelCase<'foo'>, 'foo'>>,
119+
Assert<Equal<CamelCase<'FooBARb'>, 'fooBARb'>>,
120+
Assert<Equal<CamelCase<'foo_bar-baz/qux'>, 'fooBarBazQux'>>,
121+
// array
122+
Assert<Equal<CamelCase<['Foo', 'Bar']>, 'fooBar'>>,
123+
124+
// JoinByCase
125+
Assert<Equal<JoinByCase<string, '-'>, string>>,
126+
Assert<Equal<JoinByCase<string[], '-'>, string>>,
127+
// string
128+
Assert<Equal<JoinByCase<'', '-'>, ''>>,
129+
Assert<Equal<JoinByCase<'foo', '-'>, 'foo'>>,
130+
Assert<Equal<JoinByCase<'FooBARb', '-'>, 'foo-ba-rb'>>,
131+
Assert<Equal<JoinByCase<'foo_bar-baz/qux', '-'>, 'foo-bar-baz-qux'>>,
132+
// array
133+
Assert<Equal<JoinByCase<['Foo', 'Bar'], '-'>, 'foo-bar'>>,
134+
];
135+
/* eslint-enable @typescript-eslint/no-unused-vars */

‎test/scule.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('splitByCase', () => {
2525

2626
describe('pascalCase', () => {
2727
test.each([
28+
['', ''],
2829
['foo', 'Foo'],
2930
['foo-bAr', 'FooBAr'],
3031
['FooBARb', 'FooBARb'],
@@ -45,6 +46,7 @@ describe('camelCase', () => {
4546

4647
describe('kebabCase', () => {
4748
test.each([
49+
['', ''],
4850
['foo', 'foo'],
4951
['foo/Bar', 'foo-bar'],
5052
['foo-bAr', 'foo-b-ar'],

0 commit comments

Comments
 (0)
Please sign in to comment.