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

deprecates Identity - use newtype instead #167

Merged
merged 2 commits into from
Jul 21, 2024
Merged

Conversation

ahrjarrett
Copy link
Owner

@ahrjarrett ahrjarrett commented Jul 21, 2024

changelog

It also re-names Identity to newtype, to give users a signal as to its intended purpose.

new features

  • adds "newtype" string.literal
  • adds "newtype" string.decodedURI
  • adds "newtype" string.encodedURI
  • adds string.decodedURIComponent
  • adds string.encodedURIComponent

deprecations

  • deprecates: Identity -- use newtype instead
    This release adds a handful of "newtypes". These newtypes behave a bit differently than branded or flavored types, since they (in the examples that this PR introduces) are simply interfaces that wrap the native string type.

This is accomplished by wrapping the naked type parameter in a wrapper function called (in this library) id. id is just the identity function: it simply gives back what its given.

type id<x> = x
interface newtype<type extends any.nonnullable = {}> extends id<type> {}

Now that we have newtype, we can implement string.literal:

interface string_literal<type extends string = string> extends newtype<string> { 
    toString(): type
    valueOf(): type
}

declare namespace string {
    export { string_literal as literal }
}

Notice that **we're not passing the type parameter to newtype. We're passing the universal string type (string).

By doing that, we're choosing a prototype that values with this type should have available.

To make this type more useful, we override the toString and valueOf methods, so that they return the narrowed type of type.

There! We've now implemented a custom string type, and overridden one of its methods, toString, to return a custom type.

All of its behavior is the same; all we've done is told the TS compiler that we want it to track some extra information at the typelevel.

We could take this further -- I'm not sure yet if this is a good idea or not, but for completeness, let's try overridding String.prototype.concat:

  interface string_literal<T extends string = string> extends newtype<string> { 
    toString(): T, 
    valueOf(): T, 
    concat<U extends string>(postfix: U): string.literal<`${T}${U}`>
  }

Notice that we're re-wrapping the output in string.literal, that way we can continue concatenating if we wanted without having to re-wrap the output in string.literal.

Here's what using it looks like:

string literal concat

That's awesome. Until today I didn't know this kind of thing was possible in the TypeScript type system -- as far as I know, this kind of thing isn't being done anywhere else (but I'd love to hear about it if you know of other libraries doing this!).

One last problem we need to take care of.

When we override String.prototype.concat, we get a new TypeError:

Screenshot 2024-07-20 at 9 37 30 PM

This TypeError actually makes sense: in fact, that's the whole reason we're creating these newtypes, is so that the type system can tell the difference between a string and the new type that we're creating.

In this case though the TypeError is undesirable, since we're intentionally overriding its type-level behavior.

How can we get our new concat to play nice with the global String prototype?

The trick is to simply say that we don't want to use String.prototype.concat's type signature at all:

interface string_literal<T extends string = string> extends newtype<globalThis.Omit<string, "concat">> { 
    toString(): t
    valueOf(): t
    concat<U extends string>(postfix: u): string.literal<`${T}${U}`>
}

And with that, we've implemented a new type whose type-level behavior inherits from the native string type, but preserves type information even while concatting.

I hope these release notes were useful, or at least interesting, and as always, thanks for keeping an open mind as we explore these things together.

Copy link

changeset-bot bot commented Jul 21, 2024

🦋 Changeset detected

Latest commit: 4234c81

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
any-ts Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ahrjarrett ahrjarrett merged commit 4c02264 into main Jul 21, 2024
1 check passed
@github-actions github-actions bot mentioned this pull request Jul 21, 2024
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.

1 participant