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
Closed

Conversation

jly36963
Copy link
Contributor

@jly36963 jly36963 commented Oct 12, 2023

Description

Handle literal and string types differently:

  • Current behavior: string-ts types handle string literal type inputs correctly, but return incorrect types if the input type is string
  • Desired behavior: correctly handle string type input

@jly36963
Copy link
Contributor Author

jly36963 commented Oct 12, 2023

Related to: #79

@ArthurKa is this what you were describing?

Non-literal string has bad type:

Screenshot 2023-10-11 at 6 43 08 PM

Asserting that length of string is number:

Screenshot 2023-10-11 at 6 45 16 PM

I started with a fix for Length, I'm going to add tests wherever missing to expose any similar issues

@@ -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

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

@jly36963 jly36963 marked this pull request as ready for review October 12, 2023 05:44
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


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

@@ -0,0 +1,15 @@
export type UncapitalizeSTS<T extends string> = string extends T
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

Comment on lines 12 to 13
export function uncapitalize<T extends string>(str: T): Uncapitalize<T> {
export function uncapitalize<T extends string>(str: T): UncapitalizeSTS<T> {
Copy link
Owner

Choose a reason for hiding this comment

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

We will have to export these type utilities as well :/

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.

I can update (to add these exports) during lunch today -- let me know if you have a preference for names. I chose STS as an arbitrary suffix, and I'm open to suggestions

Copy link
Owner

Choose a reason for hiding this comment

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

@p9f any opinions here? Maybe we should leave the changes in these functions to another PR so we can discuss.
If we export these types we must think through, if we ship with different types we are gonna be breaking a concept of matching type and runtime.
I'm thinking if there's a way to make our type utilities behave more like those of TS and leave them unchanged?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if we ship with different types we are gonna be breaking a concept of matching type and runtime.

i am not sure to understand this part :/ afaiu this whole PR is making the type more precise for non literal string - but where do we have an inconsistency between runtime and types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think our types will work with string types (eg: CamelCase<string>) without fixing the bad behavior that TS built-in functions (eg: Uppercase) have.

Any utility that uses a built-in func will have a return type like Uppercase<string> instead of string. This is weird. Also, we pipe types in a lot of places, so it will cause issues where the outputs/inputs don't align between piped types

Copy link
Owner

Choose a reason for hiding this comment

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

I understand it now.. I'll think through it.
Still not convinced the value is worth the added LOCs and complexity 😅

Copy link
Contributor Author

@jly36963 jly36963 Oct 13, 2023

Choose a reason for hiding this comment

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

IMO it is a critical mistake to not handle string -- and the intrinsic/built-in string utilities cannot do that.

Uncapitalize docstring: "Convert first character of string literal type to lowercase"
This type is only meant to be used with literals. Same with the others.

@jly36963 jly36963 marked this pull request as draft October 13, 2023 12:34
@jly36963 jly36963 marked this pull request as ready for review October 14, 2023 13:38
@jly36963
Copy link
Contributor Author

Given a scenario where string is used as an input to a type utility:

Length<string> // 1

Often times we are getting an incorrect literal type as the output.
The existing type tests only consider the literal case, not string.

Possible solutions:

  • Current behavior: return (wrong) literal type
  • PR behavior: return less-specific type (often string or number) when string is used
    • Eg: Length<string> // number
  • return never
    • Eg: Length<string> // never: we only support literals
  • Other solution

Regardless of the solution, I don't think that leaving the current behavior in place is acceptable.
IIRC, 2/3 of the type utilities will return a wrong type if a non-literal is used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants