Skip to content

Commit

Permalink
fix: better literal differentiation (#107)
Browse files Browse the repository at this point in the history
* fix: better literal differentiation

* updates

* simplify

* unused imports

* readme

* number test
  • Loading branch information
jly36963 authored Nov 6, 2023
1 parent 72f3e4f commit c56783b
Show file tree
Hide file tree
Showing 47 changed files with 438 additions and 245 deletions.
5 changes: 1 addition & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
"env": {
"browser": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ TypeScript yields the best static analysis when types are highly specific.
Literals are more specific than type `string`.
This library preserves literals (and unions of literals) after transformations, unlike most existing utility libraries (and built-in string methods.)

[I still didn't get the purpose of this library 🤔](#%EF%B8%8F-interview)
[I still don't get the purpose of this library 🤔](#%EF%B8%8F-interview)

### In-depth example

Expand Down
40 changes: 14 additions & 26 deletions src/internal/internals.test.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,43 @@
import * as subject from './internals'
import type * as Subject from './internals'
import type { Reject, PascalCaseAll, DropSuffix, TupleOf } from './internals.js'
import { typeOf, pascalCaseAll } from './internals.js'

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

type testReject1 = Expect<
Equal<
Subject.Reject<['one', '', 'two', '', 'three'], ''>,
['one', 'two', 'three']
>
Equal<Reject<['one', '', 'two', '', 'three'], ''>, ['one', 'two', 'three']>
>

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>
Equal<DropSuffix<'helloWorld', 'World'>, 'hello'>
>
type testDropSuffix2 = Expect<Equal<DropSuffix<string, 'World'>, string>>
type testDropSuffix3 = Expect<Equal<DropSuffix<'helloWorld', string>, string>>

type testTupleOf1 = Expect<Equal<Subject.TupleOf<3, ' '>, [' ', ' ', ' ']>>
type testTupleOf1 = Expect<Equal<TupleOf<3, ' '>, [' ', ' ', ' ']>>
}

describe('typeOf', () => {
test('null', () => {
expect(subject.typeOf(null)).toEqual('null')
expect(typeOf(null)).toEqual('null')
})
test('object', () => {
expect(subject.typeOf({})).toEqual('object')
expect(typeOf({})).toEqual('object')
})
test('object', () => {
expect(subject.typeOf(['a', 'b', 'c'])).toEqual('array')
expect(typeOf(['a', 'b', 'c'])).toEqual('array')
})
test('string', () => {
expect(subject.typeOf('hello')).toEqual('string')
expect(typeOf('hello')).toEqual('string')
})
})

describe('pascalCaseAll', () => {
test('simple', () => {
const result = subject.pascalCaseAll(['one', 'two', 'three'])
const result = pascalCaseAll(['one', 'two', 'three'])
const expected = ['One', 'Two', 'Three']
expect(result).toEqual(expected)
})
Expand Down
1 change: 1 addition & 0 deletions src/internal/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function typeOf(t: unknown) {
}

// MAP TYPES

/**
* PascalCases all the words in a tuple of strings
*/
Expand Down
70 changes: 70 additions & 0 deletions src/internal/literals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type {
IsNumberLiteral,
IsBooleanLiteral,
Any,
All,
IsStringLiteral,
IsStringLiteralArray,
} from './literals.js'

namespace LiteralsTests {
// IsNumberLiteral
type testINL1 = Expect<Equal<true, IsNumberLiteral<5>>>
type testINL2 = Expect<Equal<false, IsNumberLiteral<number>>>

// IsBooleanLiteral
type testIBL1 = Expect<Equal<IsBooleanLiteral<true>, true>>
type testIBL2 = Expect<Equal<IsBooleanLiteral<false>, true>>
type testIBL3 = Expect<Equal<IsBooleanLiteral<boolean>, false>>

// Any
type testAny1 = Expect<Equal<Any<[true, false]>, true>>
type testAny2 = Expect<Equal<Any<[true, boolean]>, true>>
type testAny3 = Expect<Equal<Any<[false, boolean]>, false>>
type testAny4 = Expect<Equal<Any<[false, false]>, false>>
type testAny5 = Expect<Equal<Any<[]>, false>>
type testAny6 = Expect<Equal<Any<boolean[]>, false>>

// All
type testAll1 = Expect<Equal<All<[true, true]>, true>>
type testAll2 = Expect<Equal<All<[true, false]>, false>>
type testAll3 = Expect<Equal<All<[true, boolean]>, false>>
type testAll4 = Expect<Equal<All<[false, boolean]>, false>>
type testAll5 = Expect<Equal<All<[false, false]>, false>>
type testAll6 = Expect<Equal<All<[]>, true>>
type testAll7 = Expect<Equal<All<boolean[]>, false>>

// IsStringLiteral
type testISL1 = Expect<Equal<true, IsStringLiteral<'foo'>>>
type testISL2 = Expect<Equal<true, IsStringLiteral<Uppercase<'foo'>>>>
type testISL3 = Expect<
Equal<false, IsStringLiteral<Uppercase<`foo${string}`>>>
>
type testISL4 = Expect<Equal<false, IsStringLiteral<`foo${string}`>>>
type testISL5 = Expect<Equal<false, IsStringLiteral<`foo${number}`>>>
type testISL6 = Expect<Equal<false, IsStringLiteral<string>>>
type testISL7 = Expect<Equal<false, IsStringLiteral<Lowercase<string>>>>
type testISL8 = Expect<
Equal<false, IsStringLiteral<Uppercase<Lowercase<string>>>>
>
type testISL9 = Expect<Equal<true, IsStringLiteral<'abc' | 'def'>>>
type testISL10 = Expect<Equal<true, IsStringLiteral<Capitalize<'abc'>>>>
type testISL11 = Expect<Equal<true, IsStringLiteral<Uncapitalize<'abc'>>>>
type testISL12 = Expect<Equal<false, IsStringLiteral<`${string}abc`>>>
type testISL13 = Expect<
Equal<false, IsStringLiteral<'abc' | Uppercase<string>>>
>
type testISL14 = Expect<Equal<true, IsStringLiteral<`${boolean}bar`>>>
type testISL15 = Expect<Equal<true, IsStringLiteral<`${undefined}bar`>>>
type testISL16 = Expect<Equal<true, IsStringLiteral<`${null}bar`>>>
type testISL17 = Expect<Equal<false, IsStringLiteral<`${number}`>>>

// IsStringLiteralArray
type testISLA1 = Expect<Equal<IsStringLiteralArray<['foo', 'bar']>, true>>
type testISLA2 = Expect<Equal<IsStringLiteralArray<string[]>, false>>
type testISLA3 = Expect<
Equal<IsStringLiteralArray<['abc', 'def', string]>, false>
>
}

test('dummy test', () => expect(true).toBe(true))
64 changes: 64 additions & 0 deletions src/internal/literals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Returns true if input number type is a literal
*/
type IsNumberLiteral<T extends number> = [T] extends [number]
? [number] extends [T]
? false
: true
: false

type IsBooleanLiteral<T extends boolean> = [T] extends [boolean]
? [boolean] extends [T]
? false
: true
: false

/**
* Returns true if any elements in boolean array are the literal true (not false or boolean)
*/
type Any<Arr extends boolean[]> = Arr extends [
infer Head extends boolean,
...infer Rest extends boolean[],
]
? IsBooleanLiteral<Head> extends true
? Head extends true
? true
: Any<Rest>
: Any<Rest>
: false

/**
* Returns true if every element in boolean array is the literal true (not false or boolean)
*/
type All<Arr extends boolean[]> = IsBooleanLiteral<Arr[number]> extends true
? Arr extends [infer Head extends boolean, ...infer Rest extends boolean[]]
? Head extends true
? Any<Rest>
: false // Found `false` in array
: true // Empty array (or all elements have already passed test)
: false // Array/Tuple contains `boolean` type

/**
* Returns true if string input type is a literal
*/
type IsStringLiteral<T extends string> = [T] extends [string]
? [string] extends [T]
? false
: Uppercase<T> extends Uppercase<Lowercase<T>>
? Lowercase<T> extends Lowercase<Uppercase<T>>
? true
: false
: false
: false

type IsStringLiteralArray<Arr extends string[] | readonly string[]> =
IsStringLiteral<Arr[number]> extends true ? true : false

export type {
IsNumberLiteral,
IsBooleanLiteral,
Any,
All,
IsStringLiteral,
IsStringLiteralArray,
}
3 changes: 2 additions & 1 deletion src/native/char-at.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { type CharAt, charAt } from './char-at.js'
namespace TypeTests {
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>>
type test3 = Expect<Equal<CharAt<Uppercase<string>, 5>, string>>
type test4 = Expect<Equal<CharAt<'some nice string', number>, string>>

// TODO: index greater than Length<T>
// type test4 = Expect<Equal<CharAt<'some nice string', 100>, ''>>
Expand Down
16 changes: 11 additions & 5 deletions src/native/char-at.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import type { Split } from './split.js'
import type {
All,
IsStringLiteral,
IsNumberLiteral,
} from '../internal/literals.js'

/**
* Gets the character at the given index.
* T: The string to get the character from.
* index: The index of the character.
*/
export type CharAt<T extends string, index extends number> = string extends T
? string
: number extends index
? string
: Split<T>[index]
export type CharAt<T extends string, index extends number> = All<
[IsStringLiteral<T>, IsNumberLiteral<index>]
> extends true
? Split<T>[index]
: string

/**
* A strongly-typed version of `String.prototype.charAt`.
* @param str the string to get the character from.
Expand Down
3 changes: 2 additions & 1 deletion src/native/ends-with.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { type EndsWith, endsWith } from './ends-with.js'
namespace TypeTests {
type test1 = Expect<Equal<EndsWith<'abc', 'c'>, true>>
type test2 = Expect<Equal<EndsWith<string, 'c'>, boolean>>
type test3 = Expect<Equal<EndsWith<'abc', string>, boolean>>
type test3 = Expect<Equal<EndsWith<Uppercase<string>, 'c'>, boolean>>
type test4 = Expect<Equal<EndsWith<'abc', string>, boolean>>
}

describe('endsWith', () => {
Expand Down
23 changes: 14 additions & 9 deletions src/native/ends-with.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Math } from '../internal/math.js'
import type { Length } from './length.js'
import type { Slice } from './slice.js'
import type {
All,
IsNumberLiteral,
IsStringLiteral,
} from '../internal/literals.js'

/**
* Checks if a string ends with another string.
Expand All @@ -12,15 +17,15 @@ export type EndsWith<
T extends string,
S extends string,
P extends number = Length<T>,
> = string extends T | S
? boolean
: Math.IsNegative<P> extends false
? P extends Length<T>
? S extends Slice<T, Math.Subtract<Length<T>, Length<S>>, Length<T>>
? true
: false
: EndsWith<Slice<T, 0, P>, S, Length<T>> // P !== T.length, slice
: false // P is negative, false
> = All<[IsStringLiteral<T | S>, IsNumberLiteral<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
: false
: EndsWith<Slice<T, 0, P>, S, Length<T>> // P !== T.length, slice
: false // P is negative, false
: boolean

/**
* A strongly-typed version of `String.prototype.endsWith`.
Expand Down
3 changes: 2 additions & 1 deletion src/native/join.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace TypeTests {
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>>
type test3 = Expect<Equal<Join<Uppercase<string>[], ' '>, string>>
type test4 = Expect<Equal<Join<['some', 'nice', 'string'], string>, string>>
}

describe('join', () => {
Expand Down
22 changes: 13 additions & 9 deletions src/native/join.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import type {
IsStringLiteral,
IsStringLiteralArray,
All,
} from '../internal/literals.js'

/**
* Joins a tuple of strings with the given delimiter.
* T: The tuple of strings to join.
Expand All @@ -6,18 +12,16 @@
export type Join<
T extends readonly string[],
delimiter extends string = '',
> = string[] extends T
? string
: string extends delimiter
? string
: T extends readonly [
> = All<[IsStringLiteralArray<T>, IsStringLiteral<delimiter>]> extends true
? T extends readonly [
infer first extends string,
...infer rest extends string[],
]
? rest extends []
? first
: `${first}${delimiter}${Join<rest, delimiter>}`
: ''
? rest extends []
? first
: `${first}${delimiter}${Join<rest, delimiter>}`
: ''
: string

/**
* A strongly-typed version of `Array.prototype.join`.
Expand Down
3 changes: 2 additions & 1 deletion src/native/length.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { type Length, length } from './length.js'

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

describe('length', () => {
Expand Down
7 changes: 4 additions & 3 deletions src/native/length.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Split } from './split.js'
import type { IsStringLiteral } from '../internal/literals.js'

/**
* Gets the length of a string.
*/
export type Length<T extends string> = string extends T
? number
: Split<T>['length']
export type Length<T extends string> = IsStringLiteral<T> extends true
? Split<T>['length']
: number
/**
* A strongly-typed version of `String.prototype.length`.
* @param str the string to get the length from.
Expand Down
Loading

0 comments on commit c56783b

Please sign in to comment.