Skip to content

Commit

Permalink
feat: Add strongly-typed padEnd and padStart alternatives
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavoguichard committed Oct 5, 2023
1 parent bb0a7c2 commit bd5797c
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 16 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ npm install string-ts
- [endsWith](#endsWith)
- [join](#join)
- [length](#length)
- [padEnd](#padend)
- [padStart](#padstart)
- [repeat](#repeat)
- [replace](#replace)
- [replaceAll](#replaceall)
Expand Down Expand Up @@ -225,6 +227,30 @@ const result = length(str)
// ^ 5
```

### padEnd

This function is a strongly-typed counterpart of `String.prototype.padEnd`.

```ts
import { padEnd } from 'string-ts'

const str = 'hello'
const result = padEnd(str, 10, '=')
// ^ 'hello====='
```

### padStart

This function is a strongly-typed counterpart of `String.prototype.padStart`.

```ts
import { padStart } from 'string-ts'

const str = 'hello'
const result = padStart(str, 10, '=')
// ^ '=====hello'
```

### repeat

This function is a strongly-typed counterpart of `String.prototype.repeat`.
Expand Down Expand Up @@ -683,6 +709,8 @@ St.Concat<['a', 'bc', 'def']> // 'abcdef'
St.EndsWith<'abc', 'c'> // true
St.Join<['hello', 'world'], '-'> // 'hello-world'
St.Length<'hello'> // 5
St.PadEnd<'hello', 10, '='> // 'hello====='
St.PadStart<'hello', 10, '='> // '=====hello'
St.Repeat<'abc', 3> // 'abcabcabc'
St.Replace<'hello-world', 'l', '1'> // 'he1lo-world'
St.ReplaceAll<'hello-world', 'l', '1'> // 'he11o-wor1d'
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type {
EndsWith,
Join,
Length,
PadEnd,
PadStart,
Repeat,
Replace,
ReplaceAll,
Expand All @@ -21,6 +23,8 @@ export {
endsWith,
join,
length,
padEnd,
padStart,
repeat,
replace,
replaceAll,
Expand Down
8 changes: 4 additions & 4 deletions src/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ namespace Math {
B extends number,
> = TupleOf<A> extends [...infer U, ...TupleOf<B>] ? U['length'] : 0

export type IsPositive<T extends number> = `${T}` extends `-${number}`
? false
: true
export type IsNegative<T extends number> = `${T}` extends `-${number}`
? true
: false

export type Abs<T extends number> =
`${T}` extends `-${infer U extends number}` ? U : T

export type GetPositiveIndex<
T extends string,
I extends number,
> = IsPositive<I> extends true ? I : Subtract<Length<T>, Abs<I>>
> = IsNegative<I> extends false ? I : Subtract<Length<T>, Abs<I>>
}

export type { Math }
88 changes: 81 additions & 7 deletions src/primitives.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,81 @@ describe('primitives', () => {
})
})

test('repeat', () => {
describe('padEnd', () => {
test('should pad a string at the end', () => {
const data = 'hello'
const result = subject.padEnd(data, 10)
expect(result).toEqual('hello ')
type test = Expect<Equal<typeof result, 'hello '>>
})

test('should pad with a given string', () => {
const data = 'hello'
const result = subject.padEnd(data, 10, '=>')
expect(result).toEqual('hello=>=>=')
type test = Expect<Equal<typeof result, 'hello=>=>='>>
})

test('should not pad if no arguments are given', () => {
const data = 'hello'
const result = subject.padEnd(data)
expect(result).toEqual('hello')
type test = Expect<Equal<typeof result, 'hello'>>
})

test('should not pad if length is shorter than string', () => {
const data = 'hello'
const result = subject.padEnd(data, 4, '=')
expect(result).toEqual('hello')
type test = Expect<Equal<typeof result, 'hello'>>
})

test('should not pad for negative numbers', () => {
const data = 'hello'
const result = subject.padEnd(data, -1, '=')
expect(result).toEqual('hello')
type test = Expect<Equal<typeof result, 'hello'>>
})
})

describe('padStart', () => {
test('should pad a string at the start', () => {
const data = 'hello'
const result = subject.padStart(data, 10)
expect(result).toEqual(' hello')
type test = Expect<Equal<typeof result, ' hello'>>
})

test('should pad with a given string', () => {
const data = 'hello'
const result = subject.padStart(data, 10, '=>')
expect(result).toEqual('=>=>=hello')
type test = Expect<Equal<typeof result, '=>=>=hello'>>
})

test('should not pad if no arguments are given', () => {
const data = 'hello'
const result = subject.padStart(data)
expect(result).toEqual('hello')
type test = Expect<Equal<typeof result, 'hello'>>
})

test('should not pad if length is shorter than string', () => {
const data = 'hello'
const result = subject.padStart(data, 4, '=')
expect(result).toEqual('hello')
type test = Expect<Equal<typeof result, 'hello'>>
})

test('should not pad for negative numbers', () => {
const data = 'hello'
const result = subject.padStart(data, -1, '=')
expect(result).toEqual('hello')
type test = Expect<Equal<typeof result, 'hello'>>
})
})

describe('repeat', () => {
test('should repeat the string by a given number of times', () => {
const data = 'abc'
const result = subject.repeat(data, 3)
Expand All @@ -101,7 +175,7 @@ describe('primitives', () => {
})
})

test('replace', () => {
describe('replace', () => {
test('should replace chars in a string at both type level and runtime level once', () => {
const data = 'some nice string'
const result = subject.replace(data, ' ')
Expand All @@ -110,7 +184,7 @@ describe('primitives', () => {
})
})

test('replaceAll', () => {
describe('replaceAll', () => {
test('should replace all chars in a string at both type level and runtime level once', () => {
const data = 'some nice string'
const result = subject.replaceAll(data, ' ')
Expand Down Expand Up @@ -190,7 +264,7 @@ describe('primitives', () => {
})
})

test('split', () => {
describe('split', () => {
test('should split a string by a delimiter into an array of substrings', () => {
const data = 'some nice string'
const result = subject.split(data, ' ')
Expand Down Expand Up @@ -296,7 +370,7 @@ describe('primitives', () => {
})
})

test('trimStart', () => {
describe('trimStart', () => {
test('should trim the start of a string at both type level and runtime level', () => {
const data = ' some nice string '
const result = subject.trimStart(data)
Expand All @@ -305,7 +379,7 @@ describe('primitives', () => {
})
})

test('trimEnd', () => {
describe('trimEnd', () => {
test('should trim the end of a string at both type level and runtime level', () => {
const data = ' some nice string '
const result = subject.trimEnd(data)
Expand All @@ -314,7 +388,7 @@ describe('primitives', () => {
})
})

test('trim', () => {
describe('trim', () => {
test('should trim a string at both type level and runtime level', () => {
const data = ' some nice string '
const result = subject.trim(data)
Expand Down
76 changes: 71 additions & 5 deletions src/primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type EndsWith<
T extends string,
S extends string,
P extends number = Length<T>,
> = Math.IsPositive<P> extends true
> = Math.IsNegative<P> extends false
? P extends Length<T>
? S extends Slice<T, Math.Subtract<Length<T>, Length<S>>, Length<T>>
? true
Expand Down Expand Up @@ -118,15 +118,77 @@ function length<T extends string>(str: T) {
return str.length as Length<T>
}

/**
* Pads a string at the end with another string.
* T: The string to pad.
* times: The number of times to pad.
* pad: The string to pad with.
*/
type PadEnd<
T extends string,
times extends number = 0,
pad extends string = ' ',
> = Math.IsNegative<times> extends false
? Math.Subtract<times, Length<T>> extends infer missing extends number
? `${T}${Slice<Repeat<pad, missing>, 0, missing>}`
: never
: T
/**
* A strongly-typed version of `String.prototype.padEnd`.
* @param str the string to pad.
* @param length the length to pad.
* @param pad the string to pad with.
* @returns the padded string in both type level and runtime.
* @example padEnd('hello', 10, '=') // 'hello====='
*/
function padEnd<T extends string, N extends number = 0, U extends string = ' '>(
str: T,
length: N = 0 as N,
pad: U = ' ' as U,
) {
return str.padEnd(length, pad) as PadEnd<T, N, U>
}

/**
* Pads a string at the start with another string.
* T: The string to pad.
* times: The number of times to pad.
* pad: The string to pad with.
*/
type PadStart<
T extends string,
times extends number = 0,
pad extends string = ' ',
> = Math.IsNegative<times> extends false
? Math.Subtract<times, Length<T>> extends infer missing extends number
? `${Slice<Repeat<pad, missing>, 0, missing>}${T}`
: never
: T
/**
* A strongly-typed version of `String.prototype.padStart`.
* @param str the string to pad.
* @param length the length to pad.
* @param pad the string to pad with.
* @returns the padded string in both type level and runtime.
* @example padStart('hello', 10, '=') // '=====hello'
*/
function padStart<
T extends string,
N extends number = 0,
U extends string = ' ',
>(str: T, length: N = 0 as N, pad: U = ' ' as U) {
return str.padStart(length, pad) as PadStart<T, N, U>
}

/**
* Repeats a string N times.
* T: The string to repeat.
* N: The number of times to repeat.
*/
type Repeat<T extends string, N extends number = 0> = N extends 0
type Repeat<T extends string, times extends number = 0> = times extends 0
? ''
: Math.IsPositive<N> extends true
? Join<TupleOf<N, T>>
: Math.IsNegative<times> extends false
? Join<TupleOf<times, T>>
: never
/**
* A strongly-typed version of `String.prototype.repeat`.
Expand Down Expand Up @@ -286,7 +348,7 @@ type StartsWith<
T extends string,
S extends string,
P extends number = 0,
> = Math.IsPositive<P> extends true
> = Math.IsNegative<P> extends false
? P extends 0
? T extends `${S}${string}`
? true
Expand Down Expand Up @@ -364,6 +426,8 @@ export type {
EndsWith,
Join,
Length,
PadEnd,
PadStart,
Repeat,
Replace,
ReplaceAll,
Expand All @@ -380,6 +444,8 @@ export {
endsWith,
join,
length,
padEnd,
padStart,
repeat,
replace,
replaceAll,
Expand Down

0 comments on commit bd5797c

Please sign in to comment.