Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle non-literal strings #80

Closed
wants to merge 16 commits into from
2 changes: 2 additions & 0 deletions src/internal/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const WEIRD_TEXT =
' someWeird-cased$*String1986Foo [Bar] W_FOR_WUMBO...' as const

export type WeirdTextUnion = typeof WEIRD_TEXT | 'dont.distribute unions'

export type Dict<T> = { [key: string]: T }
28 changes: 24 additions & 4 deletions src/internal/internals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,43 @@ import * as subject from './internals'
import type * as Subject from './internals'

namespace Internals {
type test = Expect<
type testPascalCaseAll1 = Expect<
Equal<
Subject.PascalCaseAll<['one', 'two', 'three']>,
['One', 'Two', 'Three']
>
>
type testPascalCaseAll2 = Expect<
Equal<Subject.PascalCaseAll<string[]>, string[]>
>

type test1 = Expect<
type testReject1 = Expect<
Equal<
Subject.Reject<['one', '', 'two', '', 'three'], ''>,
['one', 'two', 'three']
>
>
// // TODO: fix
Copy link
Contributor Author

@jly36963 jly36963 Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay with leaving this one as unresolved for now, as Reject is not exported

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, and also I'm ok about all tests in this file as they don't have runtime counterparts

// type testReject2 = Expect<Equal<Subject.Reject<string[], ''>, string[]>>
// type testReject3 = Expect<
// Equal<Subject.Reject<['one', 'two', 'three'], string>, string[]>
// >

type testDropSuffix1 = Expect<
Equal<Subject.DropSuffix<'helloWorld', 'World'>, 'hello'>
>
type testDropSuffix2 = Expect<
Equal<Subject.DropSuffix<string, 'World'>, string>
>
type testDropSuffix3 = Expect<
Equal<Subject.DropSuffix<'helloWorld', string>, string>
>

type test2 = Expect<Equal<Subject.DropSuffix<'helloWorld', 'World'>, 'hello'>>
type testTupleOf1 = Expect<Equal<Subject.TupleOf<3, ' '>, [' ', ' ', ' ']>>

type test3 = Expect<Equal<Subject.TupleOf<3, ' '>, [' ', ' ', ' ']>>
// // TODO: fix this
// type testTupleOf2 = Expect<Equal<Subject.TupleOf<number, ' '>, string[]>>
// type testTupleOf3 = Expect<Equal<Subject.TupleOf<3, string>, string[]>>
}

describe('typeOf', () => {
Expand Down
13 changes: 8 additions & 5 deletions src/internal/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ function typeOf(t: unknown) {
/**
* PascalCases all the words in a tuple of strings
*/
type PascalCaseAll<T extends string[]> = T extends [
infer head extends string,
...infer rest extends string[],
]
type PascalCaseAll<T extends string[]> = string[] extends T
? string[]
: T extends [infer head extends string, ...infer rest extends string[]]
? [Capitalize<Lowercase<head>>, ...PascalCaseAll<rest>]
: T

Expand All @@ -45,7 +44,11 @@ type Reject<tuple, cond, output extends any[] = []> = tuple extends [
type DropSuffix<
sentence extends string,
suffix extends string,
> = sentence extends `${infer rest}${suffix}` ? rest : sentence
> = string extends sentence | suffix
? string
: sentence extends `${infer rest}${suffix}`
? rest
: sentence

/**
* Returns a tuple of the given length with the given type.
Expand Down
22 changes: 22 additions & 0 deletions src/internal/intrinsic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {
UncapitalizeSTS,
LowercaseSTS,
UppercaseSTS,
CapitalizeSTS,
} from './intrinsic.js'

namespace Internals {
type UncapitalizeSTS1 = Expect<Equal<UncapitalizeSTS<'ABC'>, 'aBC'>>
type UncapitalizeSTS2 = Expect<Equal<UncapitalizeSTS<string>, string>>

type LowercaseSTS1 = Expect<Equal<LowercaseSTS<'ABC'>, 'abc'>>
type LowercaseSTS2 = Expect<Equal<LowercaseSTS<string>, string>>

type UppercaseSTS1 = Expect<Equal<UppercaseSTS<'abc'>, 'ABC'>>
type UppercaseSTS2 = Expect<Equal<UppercaseSTS<string>, string>>

type CapitalizeSTS1 = Expect<Equal<CapitalizeSTS<'abc'>, 'Abc'>>
type CapitalizeSTS2 = Expect<Equal<CapitalizeSTS<string>, string>>
}

test('dummy test', () => expect(true).toBe(true))
15 changes: 15 additions & 0 deletions src/internal/intrinsic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type UncapitalizeSTS<T extends string> = string extends T
Copy link
Contributor Author

@jly36963 jly36963 Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this file is necessary.

The built-in Uncapitalize/Lowercase/Uppercase/Capitalize types have what (IMO) is bad behavior

type CapitalizeSTS1 = Expect<Equal<CapitalizeSTS<'abc'>, 'Abc'>>
type CapitalizeSTS2 = Expect<Equal<CapitalizeSTS<string>, string>>

type bad1 = Expect<Equal<Capitalize<'abc'>, 'Abc'>>
type bad2 = Expect<Equal<Capitalize<string>, Capitalize<string>>>
//                                             ^ WTF

The bad2 type test showcases the "bad behavior" around the built-in types.

Why doesn't Capitalize<string> resolve to string?

This unfortunate behavior affects everything downstream (ie: word casing and object keys). We need utilities that work on both string and literal types, so I had to replace the built-in casing types with new types that would work correctly for both string and literal type inputs.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the answer but that makes me feel better about our functions also not being prepared to handle loose types

? string
: Uncapitalize<T>

export type LowercaseSTS<T extends string> = string extends T
? string
: Lowercase<T>

export type UppercaseSTS<T extends string> = string extends T
? string
: Uppercase<T>

export type CapitalizeSTS<T extends string> = string extends T
? string
: Capitalize<T>
7 changes: 7 additions & 0 deletions src/internal/math.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@ namespace MathTest {
// NOTE: `Subtract` only supports non-negative integers
type testSubtract1 = Expect<Equal<Math.Subtract<2, 1>, 1>>
type testSubtract2 = Expect<Equal<Math.Subtract<2, 2>, 0>>
type testSubtract3 = Expect<Equal<Math.Subtract<number, 2>, number>>
type testSubtract4 = Expect<Equal<Math.Subtract<2, number>, number>>

type testIsNegative1 = Expect<Equal<Math.IsNegative<2>, false>>
type testIsNegative2 = Expect<Equal<Math.IsNegative<0>, false>>
type testIsNegative3 = Expect<Equal<Math.IsNegative<-1>, true>>
type testIsNegative4 = Expect<Equal<Math.IsNegative<number>, boolean>>

type testAbs1 = Expect<Equal<Math.Abs<-1>, 1>>
type testAbs2 = Expect<Equal<Math.Abs<1>, 1>>
type testAbs3 = Expect<Equal<Math.Abs<0>, 0>>
type testAbs4 = Expect<Equal<Math.Abs<-0>, 0>>
type testAbs5 = Expect<Equal<Math.Abs<number>, number>>

type testGetPositiveIndex1 = Expect<
Equal<Math.GetPositiveIndex<'abc', -1>, 2>
>
type testGetPositiveIndex2 = Expect<
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be ok with leaving these methods simpler since they are only used by us internally

Equal<Math.GetPositiveIndex<string, -1>, number>
>
}

test('dummy test', () => expect(true).toBe(true))
15 changes: 10 additions & 5 deletions src/internal/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import type { Length } from '../native/length.js'
import type { TupleOf } from './internals.js'

namespace Math {
export type Subtract<
A extends number,
B extends number,
> = TupleOf<A> extends [...infer U, ...TupleOf<B>] ? U['length'] : 0
export type Subtract<A extends number, B extends number> = number extends
| A
| B
? number
: TupleOf<A> extends [...infer U, ...TupleOf<B>]
? U['length']
: 0

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

Expand Down
7 changes: 6 additions & 1 deletion src/native/char-at.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { type CharAt, charAt } from './char-at.js'

namespace TypeTests {
type test = Expect<Equal<CharAt<'some nice string', 5>, 'n'>>
type test1 = Expect<Equal<CharAt<'some nice string', 5>, 'n'>>
type test2 = Expect<Equal<CharAt<string, 5>, string>>
type test3 = Expect<Equal<CharAt<'some nice string', number>, string>>

// TODO: index greater than Length<T>
// type test4 = Expect<Equal<CharAt<'some nice string', 100>, ''>>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gonna return undefined which is equal to runtime

Suggested change
// type test4 = Expect<Equal<CharAt<'some nice string', 100>, ''>>
// type test4 = Expect<Equal<CharAt<'some nice string', 100>, undefined>>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I thought as well, but:

Screenshot 2023-10-12 at 8 12 20 AM

}

describe('charAt', () => {
Expand Down
14 changes: 8 additions & 6 deletions src/native/char-at.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import type { Split } from './split.js'
* T: The string to get the character from.
* index: The index of the character.
*/
export type CharAt<T extends string, index extends number> = Split<T>[index]
export type CharAt<T extends string, index extends number> = string extends T
? string
: number extends index
? string
: Split<T>[index]

/**
* A strongly-typed version of `String.prototype.charAt`.
* @param str the string to get the character from.
* @param index the index of the character.
* @returns the character in both type level and runtime.
* @example charAt('hello world', 6) // 'w'
*/
export function charAt<T extends string, I extends number>(
str: T,
index: I,
): CharAt<T, I> {
return str.charAt(index)
export function charAt<T extends string, I extends number>(str: T, index: I) {
return str.charAt(index) as CharAt<T, I>
}
4 changes: 3 additions & 1 deletion src/native/concat.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { type Concat, concat } from './concat.js'

namespace TypeTests {
type test = Expect<
type test1 = Expect<Equal<Concat<['a', 'bc', 'def']>, 'abcdef'>>
type test2 = Expect<
Equal<Concat<['a', 'bc', 'def'] | ['1', '23', '456']>, 'abcdef' | '123456'>
>
type test3 = Expect<Equal<Concat<string[]>, string>>
}

describe('concat', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/native/ends-with.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { type EndsWith, endsWith } from './ends-with.js'

namespace TypeTests {
type test = Expect<Equal<EndsWith<'abc', 'c'>, true>>
type test1 = Expect<Equal<EndsWith<string, 'c'>, boolean>>
type test2 = Expect<Equal<EndsWith<'abc', string>, boolean>>
}

describe('endsWith', () => {
Expand Down
6 changes: 5 additions & 1 deletion src/native/ends-with.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export type EndsWith<
T extends string,
S extends string,
P extends number = Length<T>,
> = Math.IsNegative<P> extends false
> = string extends T
? boolean
: string extends S
? boolean
: Math.IsNegative<P> extends false
? P extends Length<T>
? S extends Slice<T, Math.Subtract<Length<T>, Length<S>>, Length<T>>
? true
Expand Down
4 changes: 3 additions & 1 deletion src/native/includes.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { type Includes, includes } from './includes.js'

namespace TypeTests {
type test = Expect<Equal<Includes<'abcde', 'bcd'>, true>>
type test1 = Expect<Equal<Includes<'abcde', 'bcd'>, true>>
type test2 = Expect<Equal<Includes<string, 'bcd'>, boolean>>
type test3 = Expect<Equal<Includes<'abcde', string>, boolean>>
}

describe('includes', () => {
Expand Down
6 changes: 5 additions & 1 deletion src/native/includes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export type Includes<
T extends string,
S extends string,
P extends number = 0,
> = Math.IsNegative<P> extends false
> = string extends T
? boolean
: string extends S
? boolean
: Math.IsNegative<P> extends false
? P extends 0
? T extends `${string}${S}${string}`
? true
Expand Down
4 changes: 3 additions & 1 deletion src/native/join.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { type Join, join } from './join.js'

namespace TypeTests {
type test = Expect<
type test1 = Expect<
Equal<Join<['some', 'nice', 'string'], ' '>, 'some nice string'>
>
type test2 = Expect<Equal<Join<string[], ' '>, string>>
type test3 = Expect<Equal<Join<['some', 'nice', 'string'], string>, string>>
}

describe('join', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/native/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export type Join<
T extends readonly string[],
delimiter extends string = '',
> = string[] extends T
? string // Avoid spending resources on a wide type
? string
: string extends delimiter
? string
: T extends readonly [
infer first extends string,
...infer rest extends string[],
Expand Down
1 change: 1 addition & 0 deletions src/native/length.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type Length, length } from './length.js'

namespace TypeTests {
type test = Expect<Equal<Length<'some nice string'>, 16>>
type test1 = Expect<Equal<Length<string>, number>>
}

describe('length', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/native/length.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import type { Split } from './split.js'
/**
* Gets the length of a string.
*/
export type Length<T extends string> = Split<T>['length']
export type Length<T extends string> = string extends T
? number
: Split<T>['length']
/**
* A strongly-typed version of `String.prototype.length`.
* @param str the string to get the length from.
Expand Down
9 changes: 8 additions & 1 deletion src/native/pad-end.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { padEnd } from './pad-end.js'
import { type PadEnd, padEnd } from './pad-end.js'

namespace TypeTests {
type test1 = Expect<Equal<PadEnd<'hello', 10, ' '>, 'hello '>>
type test2 = Expect<Equal<PadEnd<string, 10, ' '>, string>>
type test3 = Expect<Equal<PadEnd<'hello', number, ' '>, string>>
type test4 = Expect<Equal<PadEnd<'hello', 10, string>, string>>
}

describe('padEnd', () => {
test('should pad a string at the end', () => {
Expand Down
8 changes: 7 additions & 1 deletion src/native/pad-end.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export type PadEnd<
T extends string,
times extends number = 0,
pad extends string = ' ',
> = Math.IsNegative<times> extends false
> = string extends T
? string
: number extends times
? string
: string extends pad
? string
: Math.IsNegative<times> extends false
? Math.Subtract<times, Length<T>> extends infer missing extends number
? `${T}${Slice<Repeat<pad, missing>, 0, missing>}`
: never
Expand Down
9 changes: 8 additions & 1 deletion src/native/pad-start.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { padStart } from './pad-start.js'
import { type PadStart, padStart } from './pad-start.js'

namespace TypeTests {
type test1 = Expect<Equal<PadStart<'hello', 10, ' '>, ' hello'>>
type test2 = Expect<Equal<PadStart<string, 10, ' '>, string>>
type test3 = Expect<Equal<PadStart<'hello', number, ' '>, string>>
type test4 = Expect<Equal<PadStart<'hello', 10, string>, string>>
}

describe('padStart', () => {
test('should pad a string at the start', () => {
Expand Down
8 changes: 7 additions & 1 deletion src/native/pad-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export type PadStart<
T extends string,
times extends number = 0,
pad extends string = ' ',
> = Math.IsNegative<times> extends false
> = string extends T
? string
: number extends times
? string
: string extends pad
? string
: Math.IsNegative<times> extends false
? Math.Subtract<times, Length<T>> extends infer missing extends number
? `${Slice<Repeat<pad, missing>, 0, missing>}${T}`
: never
Expand Down
9 changes: 7 additions & 2 deletions src/native/repeat.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Repeat } from './repeat.js'
import { repeat } from './repeat.js'
import { type Repeat, repeat } from './repeat.js'

namespace TypeTests {
type test1 = Expect<Equal<Repeat<' ', 3>, ' '>>
type test2 = Expect<Equal<Repeat<string, 3>, string>>
type test3 = Expect<Equal<Repeat<' ', number>, string>>
}

describe('repeat', () => {
test('should repeat the string by a given number of times', () => {
Expand Down
9 changes: 8 additions & 1 deletion src/native/repeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import type { TupleOf } from '../internal/internals.js'
* T: The string to repeat.
* N: The number of times to repeat.
*/
export type Repeat<T extends string, times extends number = 0> = times extends 0
export type Repeat<
T extends string,
times extends number = 0,
> = string extends T
? string
: number extends times
? string
: times extends 0
? ''
: Math.IsNegative<times> extends false
? Join<TupleOf<times, T>>
Expand Down
Loading
Loading