Skip to content

Commit

Permalink
feat: better type supports (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
huynl-96 committed Nov 14, 2022
1 parent 3ba367a commit 236fae0
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 28 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist
package.json
78 changes: 50 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CamelCase, JoinByCase, PascalCase, SplitByCase } from './types'

const NUMBER_CHAR_RE = /[0-9]/

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

const STR_SPLITTERS = ['-', '_', '/', '.']
const STR_SPLITTERS = ['-', '_', '/', '.'] as const

export function splitByCase (str: string, splitters = STR_SPLITTERS): string[] {
/* eslint-disable no-redeclare */
export function splitByCase <T extends string> (str: T): SplitByCase<T>
export function splitByCase <T extends string, Sep extends readonly string[]> (str: T, separators: Sep): SplitByCase<T, Sep[number]>
export function splitByCase <T extends string, Sep extends readonly string[]> (str: T, separators?: Sep) {
const splitters = separators ?? STR_SPLITTERS
const parts: string[] = []

if (!str || typeof str !== 'string') {
return parts
return parts as SplitByCase<T, Sep[number]>
}

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

for (const char of str.split('')) {
// Splitter
const isSplitter = splitters.includes(char)
const isSplitter = (splitters as unknown as string).includes(char)
if (isSplitter === true) {
parts.push(buff)
buff = ''
Expand Down Expand Up @@ -58,39 +64,55 @@ export function splitByCase (str: string, splitters = STR_SPLITTERS): string[] {

parts.push(buff)

return parts
return parts as SplitByCase<T, Sep[number]>
}
/* eslint-enable no-redeclare */

export function upperFirst (str: string): string {
if (!str) {
return ''
}
return str[0].toUpperCase() + str.substring(1)
export function upperFirst <S extends string> (str: S): Capitalize<S> {
return (!str ? '' : str[0].toUpperCase() + str.substring(1)) as Capitalize<S>
}

export function lowerFirst (str: string): string {
if (!str) {
return ''
}
return str[0].toLowerCase() + str.substring(1)
export function lowerFirst <S extends string> (str: S): Uncapitalize<S> {
return (!str ? '' : str[0].toLowerCase() + str.substring(1)) as Uncapitalize<S>
}

export function pascalCase (str: string | string[] = ''): string {
return (Array.isArray(str) ? str : splitByCase(str))
.map(p => upperFirst(p))
.join('')
/* eslint-disable no-redeclare */
export function pascalCase (): ''
export function pascalCase <T extends string | readonly string[]> (str: T): PascalCase<T>
export function pascalCase <T extends string | readonly string[]> (str?: T) {
return !str
? ''
: (Array.isArray(str) ? str : splitByCase(str as string))
.map(p => upperFirst(p))
.join('') as PascalCase<T>
}
/* eslint-enable no-redeclare */

export function camelCase (str: string | string[] = ''): string {
return lowerFirst(pascalCase(str))
/* eslint-disable no-redeclare */
export function camelCase (): ''
export function camelCase <T extends string | readonly string[]> (str: T): CamelCase<T>
export function camelCase <T extends string | readonly string[]> (str?: T) {
return lowerFirst(pascalCase(str)) as CamelCase<T>
}

export function kebabCase (str: string | string[] = '', joiner = '-'): string {
return (Array.isArray(str) ? str : splitByCase(str))
.map((p = '') => p.toLowerCase())
.join(joiner)
/* eslint-enable no-redeclare */

/* eslint-disable no-redeclare */
export function kebabCase (): ''
export function kebabCase <T extends string | readonly string[]> (str: T): JoinByCase<T, '-'>
export function kebabCase <T extends string | readonly string[], Joiner extends string> (str: T, joiner: Joiner): JoinByCase<T, Joiner>
export function kebabCase <T extends string | readonly string[], Joiner extends string> (str?: T, joiner?: Joiner) {
return !str
? ''
: (Array.isArray(str) ? str : splitByCase(str as string))
.map(p => p.toLowerCase())
.join(joiner ?? '-') as JoinByCase<T, Joiner>
}
/* eslint-enable no-redeclare */

export function snakeCase (str: string | string[] = '') {
return kebabCase(str, '_')
/* eslint-disable no-redeclare */
export function snakeCase (): ''
export function snakeCase <T extends string | readonly string[]> (str: T): JoinByCase<T, '_'>
export function snakeCase <T extends string | readonly string[]> (str?: T) {
return kebabCase(str, '_') as JoinByCase<T, '_'>
}
/* eslint-enable no-redeclare */
135 changes: 135 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
type Assert<T extends true> = T
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false

type Splitter = '-' | '_' | '/' | '.'
type FirstOfString<S extends string> = S extends `${infer F}${string}` ? F : never
type RemoveFirstOfString<S extends string> = S extends `${string}${infer R}` ? R : never
type IsUpper<S extends string> = S extends Uppercase<S> ? true : false
type IsLower<S extends string> = S extends Lowercase<S> ? true : false
type SameLetterCase<X extends string, Y extends string> = IsUpper<X> extends IsUpper<Y> ? true : IsLower<X> extends IsLower<Y> ? true : false
type CapitalizedWords<T extends readonly string[], Acc extends string = ''> =
T extends readonly [infer F extends string, ...infer R extends string[]]
? CapitalizedWords<R, `${Acc}${Capitalize<F>}`>
: Acc
type JoinLowercaseWords<T extends readonly string[], Joiner extends string, Acc extends string = ''> =
T extends readonly [infer F extends string, ...infer R extends string[]]
? Acc extends ''
? JoinLowercaseWords<R, Joiner, `${Acc}${Lowercase<F>}`>
: JoinLowercaseWords<R, Joiner, `${Acc}${Joiner}${Lowercase<F>}`>
: Acc
type LastOfArray<T extends any[]> = T extends [...any, infer R] ? R : never
type RemoveLastOfArray<T extends any[]> = T extends [...infer F, any] ? F : never
export type SplitByCase<T, Sep extends string = Splitter, Acc extends unknown[] = []> =
string extends Sep
? string[]
: T extends `${infer F}${infer R}`
? [LastOfArray<Acc>] extends [never]
? SplitByCase<R, Sep, [F]>
: LastOfArray<Acc> extends string
? R extends ''
? SplitByCase<R, Sep, [...RemoveLastOfArray<Acc>, `${LastOfArray<Acc>}${F}`]>
: SameLetterCase<F, FirstOfString<R>> extends true
? F extends Sep
? FirstOfString<R> extends Sep
? SplitByCase<R, Sep, [...Acc, '']>
: IsUpper<FirstOfString<R>> extends true
? SplitByCase<RemoveFirstOfString<R>, Sep, [...Acc, FirstOfString<R>]>
: SplitByCase<R, Sep, [...Acc, '']>
: SplitByCase<R, Sep, [...RemoveLastOfArray<Acc>, `${LastOfArray<Acc>}${F}`]>
: IsLower<F> extends true
? SplitByCase<RemoveFirstOfString<R>, Sep, [...RemoveLastOfArray<Acc>, `${LastOfArray<Acc>}${F}`, FirstOfString<R>]>
: SplitByCase<R, Sep, [...Acc, F]>
: never
: Acc extends []
? T extends '' ? [] : string[]
: Acc

export type PascalCase<T> =
string extends T
? string
: string[] extends T
? string
: T extends string
? SplitByCase<T> extends readonly string[]
? CapitalizedWords<SplitByCase<T>>
: never
: T extends readonly string[]
? CapitalizedWords<T>
: never

export type CamelCase<T> =
string extends T
? string
: string[] extends T
? string
: Uncapitalize<PascalCase<T>>

export type JoinByCase<T, Joiner extends string> =
string extends T
? string
: string[] extends T
? string
: T extends string
? SplitByCase<T> extends readonly string[]
? JoinLowercaseWords<SplitByCase<T>, Joiner>
: never
: T extends readonly string[]
? JoinLowercaseWords<T, Joiner>
: never

/* eslint-disable @typescript-eslint/no-unused-vars */
type tests = [
// SplitByCase
Assert<Equal<SplitByCase<string>, string[]>>,
// default splitters
Assert<Equal<SplitByCase<''>, []>>,
Assert<Equal<SplitByCase<'foo'>, ['foo']>>,
Assert<Equal<SplitByCase<'foo_bar-baz/qux'>, ['foo', 'bar', 'baz', 'qux']>>,
Assert<Equal<SplitByCase<'foo--bar-Baz'>, ['foo', '', 'bar', 'Baz']>>,
Assert<Equal<SplitByCase<'foo123-bar'>, ['foo123', 'bar']>>,
Assert<Equal<SplitByCase<'fooBar'>, ['foo', 'Bar']>>,
Assert<Equal<SplitByCase<'fooBARBaz'>, ['foo', 'BAR', 'Baz']>>,
Assert<Equal<SplitByCase<'FOOBar'>, ['FOO', 'Bar']>>,
Assert<Equal<SplitByCase<'ALink'>, ['A', 'Link']>>,
// custom splitters
Assert<Equal<SplitByCase<'foo\\Bar.fuzz-FIZz', '\\' | '.' | '-'>, ['foo', 'Bar', 'fuzz', 'FI', 'Zz']>>,

// PascalCase
Assert<Equal<PascalCase<string>, string>>,
Assert<Equal<PascalCase<string[]>, string>>,
// string
Assert<Equal<PascalCase<''>, ''>>,
Assert<Equal<PascalCase<'foo'>, 'Foo'>>,
Assert<Equal<PascalCase<'foo-bAr'>, 'FooBAr'>>,
Assert<Equal<PascalCase<'FooBARb'>, 'FooBARb'>>,
Assert<Equal<PascalCase<'foo_bar-baz/qux'>, 'FooBarBazQux'>>,
Assert<Equal<PascalCase<'foo--bar-Baz'>, 'FooBarBaz'>>,
// array
Assert<Equal<PascalCase<['foo', 'Bar']>, 'FooBar'>>,
Assert<Equal<PascalCase<['foo', 'Bar', 'fuzz', 'FI', 'Zz']>, 'FooBarFuzzFIZz'>>,

// CamelCase
Assert<Equal<CamelCase<string>, string>>,
Assert<Equal<CamelCase<string[]>, string>>,
// string
Assert<Equal<CamelCase<''>, ''>>,
Assert<Equal<CamelCase<'foo'>, 'foo'>>,
Assert<Equal<CamelCase<'FooBARb'>, 'fooBARb'>>,
Assert<Equal<CamelCase<'foo_bar-baz/qux'>, 'fooBarBazQux'>>,
// array
Assert<Equal<CamelCase<['Foo', 'Bar']>, 'fooBar'>>,

// JoinByCase
Assert<Equal<JoinByCase<string, '-'>, string>>,
Assert<Equal<JoinByCase<string[], '-'>, string>>,
// string
Assert<Equal<JoinByCase<'', '-'>, ''>>,
Assert<Equal<JoinByCase<'foo', '-'>, 'foo'>>,
Assert<Equal<JoinByCase<'FooBARb', '-'>, 'foo-ba-rb'>>,
Assert<Equal<JoinByCase<'foo_bar-baz/qux', '-'>, 'foo-bar-baz-qux'>>,
// array
Assert<Equal<JoinByCase<['Foo', 'Bar'], '-'>, 'foo-bar'>>,
];
/* eslint-enable @typescript-eslint/no-unused-vars */
2 changes: 2 additions & 0 deletions test/scule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('splitByCase', () => {

describe('pascalCase', () => {
test.each([
['', ''],
['foo', 'Foo'],
['foo-bAr', 'FooBAr'],
['FooBARb', 'FooBARb'],
Expand All @@ -45,6 +46,7 @@ describe('camelCase', () => {

describe('kebabCase', () => {
test.each([
['', ''],
['foo', 'foo'],
['foo/Bar', 'foo-bar'],
['foo-bAr', 'foo-b-ar'],
Expand Down

0 comments on commit 236fae0

Please sign in to comment.