-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Tag types #4895
Comments
A few thoughts, in the typescript compiler we have used brands to achieve a similar behavior, see: https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts#L485; #1003 could make creating tags a little bit cleaner. A nominal type would be far cleaner solution (#202). |
one more use case just came across:
with type tag this situation could have been avoided
|
With Typescript 2 you can now simulate the behavior you want:
with this concept, you can also create Ranges:
|
IMO this feature is long over due - mainly because there are so many types of strings: UUID, e-mail address, hex color-code and not least entity-specific IDs, which would be incredibly useful when coupling entities to repositories, etc. I currently use this work-around:
This works "okay" in terms of type-checking, but leads to confusing error-messages:
The much bigger problem is, these types aren't permitted in maps, because an index signature parameter type cannot be a union type - so this isn't valid:
Is there a better work-around I don't know about? I've tried something like Is there a proposal or another feature in the works that will improve on this situation? |
I am struggling hard with this in a model I've been building this past week. The model is a graph, and nodes have input and output connectors - these connectors are identical in shape, and therefore inevitably can be assigned to each other, which is hugely problematic, since the distinction between inputs and outputs (for example when connecting them) is crucial, despite their identical shapes. I have tried work-arounds, including a discriminator Both approaches leave an unnecessary run-time footprint in the form of an extra property, which will never be used at run-time - it exists solely to satisfy the compiler. I've also attempt to simply "lie" about the existence of a discriminator or "tag" property to satisfy the compiler, since I'll never look for these at run-time anyway - that works, but it's pretty misleading to someone who doesn't know this codebase, who might think that they can use these properties to implement a run-time type-check, since that's literally what the declaration seem to say. In addition, I have a ton of different UUID key types in this model, which, presently, I only model as strings, for the same reason - which means there's absolutely no guarantee that I won't accidentally use the wrong kind of key with the wrong collection/map etc. I really hope there's a plan to address this in the future. |
Great issue. Added two thoughts here: The original usecase from @Aleksey-Bykov sounds a lot like dependent-types, implemented most famously in Idris, where you can define a type "array of n positive integers", and a function that appends two arrays and returns a third array of (n+m) positive integers. Non-empty is a simple case of that. If you like this power, take a look at https://www.idris-lang.org/example/ The simpler usecase from @mindplay-dk is actually what I am struggling with right now. In my example, I have Uint8Array, which may be a PrivateKey or a PublicKey for a cryptographic hash function and I don't want to mix them up. Just like the input and output nodes. This was my solution...
Same for public key. When I just did I would like to see a bit longer example of how you use the UUID type, but it sounds quite similar to my approach in principle. Maybe there is a better way to deal with this. The |
@ethanfrey How about:
|
@ethanfrey @dgreensp see comments by @SimonMeskens here - the |
btw @ethanfrey, TS also does dependent types already. I use them a lot, you might want to check out https://github.com/tycho01/typical to see how. |
@dgreensp I tried what you said, but tsc complained that Uint8Array didn't have the property @mindplay-dk That solution looks perfect, that I can cleanly cast with I'm still learning typescript, and want to thank you all for being a welcoming community. |
@SimonMeskens great link. not just the repo, but its references (rambda and lens) also. I have played with dependent types with LiquidHaskell, and studied a bit of Idris, but it seems to need a symbolic algebra solved many times... Two big examples are tracking the length of an array and tracking the set of items in a container. I was looking at typical to see how they tracked that and found: https://github.com/tycho01/typical/blob/master/src/array/IncIndexNumbObj.ts commented out.... Am I missing something? This example seems quite nice. https://github.com/gcanti/typelevel-ts#naturals But it seems they had to predefine all possible numbers to do math: https://github.com/gcanti/typelevel-ts/blob/master/src/index.ts#L66-L77 This also looks interesting https://ranjitjhala.github.io/static/refinement_types_for_typescript.pdf but seems to be a demo project and currently inactive: https://github.com/UCSD-PL/refscript |
@ethanfrey Interesting, I wonder why my IDE didn't seem to complain. Well, the bottom line is you just need to cast through
The other example you mention also uses an any-cast, on an entire function signature no less:
The general principle at work, in both cases, is that the compile-time type need not bear any particular relationship to the runtime type. Given this fact, there is very little constraining what you can do. The fact that you need a "dirty" any-cast to mark something as public/private or input/output is a feature, not a bug, because it means that only your special marker functions can do it. |
@dgreensp Ahh... the any cast did the trick. I was doing
which was complaining. but using any fixed that.
|
You can just do: const key : IPrivateKey = Buffer.from("top-secret") as any; |
I've been trying to find an elegant solution to the "primitive obsession" problem. I want my interfaces and method arguments to use data types like UniqueID, EmailAddress, and Priority rather than string, string, and number. This would lead to more type safety and better documentation. Tagging seems overly complicated to me. Type aliases ALMOST do what I want. I think I could get 99% of the way there with syntax like the following below. This is just like a type alias except you have to explicitly cast to assign a value to the new type, but implicit casting from the new type to the primitive is just fine.
== UPDATE == I've been using syntax like below and I'm getting more comfortable with it. Pretty simple once you get the hang of intersection types. I still think it would be more obvious if I could just type something like
== UPDATE == Functionally using intersection types gives me most of what I want with regard to type safety. The biggest hassle with it is that when I mouse-over certain symbols, like function names, to see what arguments they accept I see lots of |
I liked @SimonMeskens example, but it leaves the I made the following change which seems to preserve some some checks for the original type: export type Opaque<T, S extends symbol> = T & OpaqueTag<S> | OpaqueTag<S>; By adding the intersection, Typescript will not let such a variable be used as a string without a type assertion. For example: type UserId = Opaque<string, UserIdSymbol>;
const userId: UserId = "123" as UserId;
userId.replace('3', '2'); // This works on the Union type, but not the union+intersected
function takesAString(str: string) {
// ...
}
takesAString(userId); // This works on the Union type, but not the union+intersected
takesAString(userId as string); // This works on both versions
const num: number = userId as number; // this doesn't work on either version I'm sure there's probably a better way to implement this, and certainly room for either set of semantics. |
I built https://github.com/ForbesLindesay/opaque-types which uses a transpiler to support opaque and nominal types, with optional runtime validation for the |
@ForbesLindesay way to go! |
@qm3ster that looks pretty cool. The The main advantage I see to using my transpiled approach, is that it's very easy to completely change, in the event that new versions of typescript break the current approach. The secondary advantage is that I get to use clean syntax, and ensure that the type name is the same as the name of the object that provides utility functions for cast/extract. I mainly wanted to prototype how I thought they might behave if added to the language. |
@ForbesLindesay It's not my project :v I just used it a few times. |
@ProdigySim @SimonMeskens Nice solution but it seems to has some cons as below.
Then I tried improvement. And it seems working. The definition of Opaque: interface SourceTag{
readonly tag:symbol;
}
declare const OpaqueTagSymbol: unique symbol;
declare class OpaqueTag<S extends SourceTag>{
private [OpaqueTagSymbol]:S;
}
export type Opaque<T,S extends SourceTag> = T & OpaqueTag<S> | OpaqueTag<S>; usage: type UserId = Opaque<string,{ readonly tag:unique symbol}>;
type UserId2 = Opaque<string,{ readonly tag:unique symbol}>;
const userId:UserId = 'test' as UserId ;
const userId2:UserId2 = userId; // compile error The notation |
I tried out that approach and it's definitely a shorter syntax, but I think most of the time I would not be too worried about one extra line since I will create relatively few opaque types, and I will probably add other boilerplate/helpers in the type's module. One difference between the two approaches is the error message we get from typescript: From @SimonMeskens 's setup:
From @qwerty2501 's setup:
I stripped the namespace from both errors to make them more equivalent. The latter is shorter, but the former explicitly calls out |
@ProdigySim |
I checked out how Flow handles opaque types in comparison to our solutions. They have some interesting behavior. Notably:
I put together a typescript playground link demonstrating different constructions of
I think each could have uses; but a first-party Typescript solution could definitely allow the best of all worlds here. |
For others coming across this, a fairly succinct solution that results in short but readable error messages can be found over here. Comes with caveats, since it is setup to ignore a compiler warning, but so far I like the UX of it the most out of all of the options I have seen so far. Need to test it cross-module still though. |
Wrote a small tag type library based on some ideas here. Will change when TS gets nominal types. https://github.com/StephanSchmidt/taghiro Happy for feedback. |
I guess, it is necessary for typescript to support official opaque type alias. |
We don't have actual opaque types in TypeScript yet, but this serves as a stand-in for now. This is just one of several possible ways to do this in TypeScript; see these threads for many details: - https://codemix.com/opaque-types-in-javascript/ - microsoft/TypeScript#15807 - microsoft/TypeScript#4895 - microsoft/TypeScript#202
FYI: An issue with the solutions in #4895 (comment) and #4895 (comment) is that if your build process involves interfacing between different modules using .d.ts files from |
@StephanSchmidt I am using taghiro. It's very easy to use and 9t works well so far! |
@mhegazy @weswigham did this ever happen? |
There are two proposals in the form of experimental implementations in PRs. |
@weswigham sorry to raise this issue again. Is there is roadmap for this? |
I had a recent need for a tag type that none of the existing workarounds can solve. I rebased @weswigham's #33290 (at shicks/TypeScript:structural-tag) and found that it works more or less perfectly. To summarize this solution, it introduces a new type operator, Branding without the liesType branding is a very common practice, seen in libraries, numerous blogs, and even in the TypeScript codebase itself. But to this day, the standard approach (workaround, rather) is to lie to the type checker by adding fake properties: type Brand = {brand: void};
type BrandedString = string&Brand; This may be "free" at runtime, but you can get into some trouble while type checking, since this Reimagining some code from the TS codebase: type Branded<T, B> = T & tag {[K in B]: void};
export interface ClassElement extends Branded<NamedDeclaration, 'ClassElement'> {
readonly name?: PropertyName;
} Inherently structural, but nominal-friendlyThese That said, Introducing a type operator/primitive solely for the purpose of nominal typing would be going against the grain (hence the repeated complaint on #202 and elsewhere that TypeScript is structural, not nominal), but this would provide the necessary tools for those who want nominal typing to achieve it, while still remaining essentially structural. For those who want to ensure that multiple different versions of their API are compatible, they can use structural tags with fixed strings. For those who want to guarantee that nothing outside their module is assignable to their nominal type, they can use True opacityIn addition to allowing authors to avoid lies and enabling brands and nominal types, this solution introduces a new type with important capabilities that's currently impossible to express. As mentioned above, the current status quo for brands is to use an object literal type (e.g. The beauty of type SafeBigint = tag {readonly __safeBigint: unique symbol};
declare const x: string|SafeBigint;
if (typeof x === 'string') {
use(x);
// ^? const x: string | (string & SafeBigint)
} else {
use(x);
// ^? const x: SafeBigint
} Because the Final wordsI'm asking that the TypeScript team reconsider the Thank you for your consideration. |
Another library example is zod |
Problem
Details
There are situations when a value has to pass some sort of check/validation prior to being used. For example: a min/max functions can only operate on a non-empty array so there must be a check if a given array has any elements. If we pass a plain array that might as well be empty, then we need to account for such case inside the min/max functions, by doing one of the following:
This way the calling side has to deal with the consequences of min/max being called yet not being able to deliver.
Solution
A better idea is to leverage the type system to rule out a possibility of the min function being called with an empty array. In order to do so we might consider so called tag types.
A tag type is a qualifier type that indicates that some predicate about a value it is associated with holds true.
it's up to the developer in what circumstances an array gets its AsNonEmpty tag, which can be something like:
Also tags can be assigned at runtime:
As was shown in the current version (1.6) an empty const enum type can be used as a marker type (AsNonEmpty in the above example), because
However enums have their limitations:
A few more examples of what tag type can encode:
string & AsTrimmed & AsLowerCased & AsAtLeast3CharLong
number & AsNonNegative & AsEven
date & AsInWinter & AsFirstDayOfMonth
Custom types can also be augmented with tags. This is especially useful when the types are defined outside of the project and developers can't alter them.
User & AsHavingClearance
ALSO NOTE: In a way tag types are similar to boolean properties (flags), BUT they get type-erased and carry no rutime overhead whatsoever being a good example of a zero-cost abstraction.
UPDATED:
Also tag types can be used as units of measure in a way:
string & AsEmail
,string & AsFirstName
:number & In<Mhz>
,number & In<Px>
:The text was updated successfully, but these errors were encountered: