Open zpdDG4gta8XKpMCd opened 8 years ago
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; https://github.com/Microsoft/TypeScript/issues/1003 could make creating tags a little bit cleaner. A nominal type would be far cleaner solution (https://github.com/Microsoft/TypeScript/issues/202).
one more use case just came across:
var scheduled = setTimeout(function() { }, 1000);
clearInterval(scheduled); // <-- runtime bug
with type tag this situation could have been avoided
declare function setTimeout(act: () => void, delay: number) : number & AsForTimeout;
declare function clearInterval(scheduled: number & AsForInterval);
var scheduled = setTimeout(function() { }, 1000);
clearInterval(scheduled); // <-- compile error
With Typescript 2 you can now simulate the behavior you want:
declare class MinValue<T extends number> {
private __minValue: T;
}
// MinValue type guard
function hasMinValue<T extends number>(value: number, minValue: T): value is number & MinValue<T> {
return value >= minValue;
}
// Use it
function delay(fn: Function, milliSeconds: number & MinValue<0>) { }
const delayInMs = 200;
delay(() => { }, delayInMs); // error: number not assignable to MinValue<0>
if (hasMinValue(delayInMs, 0)) {
delay(() => { }, delayInMs); // OK
}
if (hasMinValue(delayInMs, 100)) {
delay(() => { }, delayInMs); // error: MinValue<100> not assignable to MinValue<0>
}
with this concept, you can also create Ranges:
// MaxValue
declare class MaxValue<T extends number> {
private __maxValue: T;
}
// MaxValue type guard
function hasMaxValue<T extends number>(value: number, maxValue: T): value is number & MaxValue<T> {
return value <= maxValue;
}
// RangeValue
type RangeValue<TMin extends number, TMax extends number> = MinValue<TMin> & MaxValue<TMax>;
// RangeValue type guard
function inRange<TMin extends number, TMax extends number>(value: number, minValue: TMin, maxValue: TMax): value is number & RangeValue<TMin, TMax> {
return value >= minValue && value >= maxValue;
}
// Range example
//-----------------
type Opacity = RangeValue<0, 1>;
function setTransparency(opacity: number & Opacity) {
// ...
}
const opacity = 0.3;
setTransparency(opacity); // error: 'number' not assignable to MinValue<0>
if (inRange(opacity, 0, 1)) {
setTransparency(opacity); // OK
}
if (inRange(opacity, 0, 3)) {
setTransparency(opacity); // error: MinValue<0> & MaxValue<3> not assignable to MinValue<0> & MaxValue<1>
}
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:
export type UUID = string & { __UUID: undefined }
This works "okay" in terms of type-checking, but leads to confusing error-messages:
Type 'string' is not assignable to type 'UUID'.
Type 'string' is not assignable to type '{ __UUID: void; }'.
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:
interface ContentCache {
[content_uuid: UUID]: string;
}
Is there a better work-around I don't know about? I've tried something like declare type UUID extends string
, which isn't permitted, and declare class UUID extends String {}
, which isn't permitted as keys in a map either.
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 type: "input" | "output"
or distinct type "tags" like __Input: void
and __Output: void
respectively.
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...
interface IPrivateKey extends Uint8Array {
readonly assertPrivateKey: undefined;
}
function asPrivate(bin: Uint8Array): IPrivateKey {
// tslint:disable-next-line:prefer-object-spread
return Object.assign(bin, { assertPrivateKey: undefined });
}
function signMessage(msg: string, secret: IPrivateKey): Uint8Array {
// ...
}
Same for public key. When I just did type IPublicKey = Uint8Array
, tsc would treat them interchangeably.
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 Object.assign
ugliness bothers me and adds runtime overhead.
@ethanfrey How about:
function asPrivate(bin: Uint8Array): IPrivateKey {
return bin as IPrivateKey
}
@ethanfrey @dgreensp see comments by @SimonMeskens here - the unique symbol
solution is very close to what I was looking for.
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 assertPrivateKey
. The problem with using ghost properties to define types.
@mindplay-dk That solution looks perfect, that I can cleanly cast with as IPrivateKey
or as IPublicKey
, and the information is carried around throughout tsc (cannot convert one to the other), but has no runtime footprint. Thank you for that link.
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 any
:
function asPrivate(bin: Uint8Array): IPrivateKey {
return bin as any
}
The other example you mention also uses an any-cast, on an entire function signature no less:
const createInputConnector:
(node: GraphNode) => InputConnector =
<any>createConnector;
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
const key : IPrivateKey = Buffer.from("top-secret") as IPrivateKey;
which was complaining. but using any fixed that.
const key : IPrivateKey = Buffer.from("top-secret") as any as IPrivateKey;
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.
type EmailAddress < string; // rather than '='
const emailAddressRegExp = new RegExp("[a-z]+@.com");
function newEmailAddress(e: string): EmailAddress {
if (!emailAddressRegExp.test(e)) {
throw new TypeError("That isn't a valid email address");
}
return new EmailAddress(e.trim()); // cast operator might be fine instead
}
let email1: EmailAddress = newEmailAddress("bob@yahoo.com"); // ok
let email2: EmailAddress = "fail"; // compile error, unsafe cast
let email3 = "banana" as EmailAddress; // compiles ok, forcing the cast
let s: string = email1; // ok implicit cast to wider type always works
== 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 type EmailAddress restricts string
.
export type UniqueId = string & { _type: "uniqueId" };
export type Priority = number & { _type: "priority" };
== 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 string & { _type: "personId" }
and this is quite ugly and hard to decipher. I really just want to see that a function accepts a PersonId
or EmailAddress
not some hacked solution to this nominal typing issue.
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
Makes me wonder why the unique
keyword is tied to the symbol
keyword?
Why can't you just say:
type Email = unique string;
type UserID = unique number;
type AuthorizedUser = unique User;
In other words, unique
simply meaning "treat the resulting type as unique", which is essentially what it means in conjunction with symbol
- this could work for any type, couldn't it?
For that matter, this seems really counter-intuitive:
declare const Foo: unique symbol;
Compared with:
type Foo = unique symbol;
Why do you have to declare a non-existent "fake" constant get the effect of a unique type in the first place?
Hmm.
@mindplay-dk This isn't really what unique
means in the context of a unique symbol. It just so happens that unique symbols can be "abused" to simulate tag types, but if it makes sense to somebody to call tag types "unique types" that is just a coincidence, as far as I'm concerned.
You could tag a numeric type with a string constant type instead of a unique symbol type:
type Foo = number & 'Foo'
The unique symbol is just taking the place of 'Foo'
here.
@mindplay-dk does have a point actually. In theory, it would be a nice addition to the language to have unique
as a keyword for nominality. Anything you slap it on becomes nominal. Once you start thinking about how that would work though, it quickly becomes clear this is a hard problem to solve and type Foo = unique symbol;
probably wouldn't be the way to do it.
@SimonMeskens Sure. Or nominal type Email = string
. I have no concrete proposal for the semantics of that. :)
Just to add in my two cents:
I've found constructs as mentioned by @mindplay-dk such as
export type UUID = string & { __UUID: undefined }
immensely useful in my codebase. I haven't found any way to resolve the main issue with this though, that the type isn't a valid type as the key for an index type. Has anyone found a way to workaround this?
And I'm obviously in favor of some cleaner way to create nominal types and/or tag types! But until then, I'd settle for any decent workarounds 😄
Does the work done for indexed types in 2.9 make it more feasible to revisit this feature now @mhegazy?
We have talked about this one a few times recently. @weswigham volunteered to write up a proposal.
Just curious, @weswigham / @mhegazy if there is a proposal for this now? Or what the general idea you're considering is even if not in proposal form?
I'm always a little hesitant in asking for features because I know how tough maintaining OSS can be, so though I'd love to see this feature, my appreciation for all the work you've done on TS is great no matter what happens!
For what it is worth, I got quite good mileage from the unique Symbol approach mentioned above:
declare const UsernameSymbol: unique symbol;
type Username = typeof UsernameSymbol;
export type UsernameString = Username & string;
declare const PasswordSymbol: unique symbol;
type Password = typeof PasswordSymbol;
export type PasswordString = Password & string;
I know if the string is a Username or a Password, and never can switch the order of them with a bad api call. The second line is not really needed, you can collapse it into the third (export type UsernameString = typeof UsernameSymbol & string;
) but my fellow devs thought this was the most readable.
It would be nice to have some more language support and easier syntax, but this works quite well, albeit a few lines in the type definitions, the usage is easy and intuitive.
unique symbol
as a tag turned out to be problematic for us, few cases:
string & typeof uniqueSymbol
is not compatible to string
how about string & { [sym]: typeof uniqueSymbol }
i like the following better:
declare class As<Tag extends string> { private 't a g': Tag; }
type UserId = string & As<'user-id'>
mhegazy commented 18 hours ago how about
string & { [sym]: typeof uniqueSymbol }
Whoa... I got lost. Why is this { [a: string]: ... }
-wrapping of typeof uniqueSymbol
necessary for string &
but not for something like MyClass &
?
@SlurpTheo you're mistaking computed properties with indexers. And none of this is "necessary", just multiple options
Since Tyepscript 2.9, these above constructs no longer work, at least the issue with
string & typeof uniqueSymbol
as that is converted to a never
type....
This is documented in #25268 and considered desired behavior by the typescript team. Any ideas on how to build tag types that still work with the stricter type rules?
@ethanfrey I use this, found on a related issue thread (sorry but I can't find the original)
declare class OpaqueTag<S extends string> {
private tag: S;
}
type Opaque<T, S extends string> = T & OpaqueTag<S>;
type UserUUID = Opaque<string, 'UserUUID'>;
type TransactionId = Opaque<string, 'TransactionId'>;
const a: UserUUID = '...' as UserUUID; // assigning a string literal requires a cast
const b: TransactionId = UserUUID // errors
It's not perfect, but in my experience gets the job done and it's really readable
@agos the problem with that solution is that it's not type safe. Symbols are the only nominal type, so you need a solution that uses them. That much is clear.
Here's the sample adapted to be type-safe:
// Using namespace here simply to show that external files
// should NOT have access to OpaqueTagSymbol or OpaqueTag.
// Put this in its own module, without the namespace
namespace Tag {
declare const OpaqueTagSymbol: unique symbol;
declare class OpaqueTag<S extends symbol> {
private [OpaqueTagSymbol]: S;
}
export type Opaque<T, S extends symbol> = T & OpaqueTag<S>;
}
// Simple alias, if in a module, same as:
// import { Opaque } from "tagmodule";
type Opaque<T, S extends symbol> = Tag.Opaque<T, S>;
// Since these are ghost symbols, you probably don't want to export them
// Create actual symbols if you do
declare const UserUUIDSymbol: unique symbol;
declare const TransactionId: unique symbol;
type UserUUID = Tag.Opaque<string, typeof UserUUIDSymbol>;
type TransactionId = Opaque<string, typeof TransactionId>;
const a: UserUUID = '...' as UserUUID; // assigning a string literal requires a cast
const b: TransactionId = a // errors
Nice solution, thank you @SimonMeskens
@SimonMeskens it seems to me that the only case where the type safety of the solution I posted earlier would be that two Tag types with the same string passed as second type parameter? In that case it might be still a worthy tradeoff
@SimonMeskens that's true but it limits the solution to where Symbol
is supported.
I hope that argument will vanish soon. :)
@sledorze I didn't use symbol at all, it's all just in declarative type land, so this solution will work even in environments without symbol :)
Magic! (of course, if you have to target an old version it might not work)
@agos That's correct, it's mostly a collision problem, but there's also the issue that with strings, you can't encapsulate creation, as any other part of the code can construct a valid one by providing the same string. With symbols, if the symbol isn't exported, only that part of the code is able to create that specific tagged type.
@SimonMeskens Oh yes! wonderful ;)
I liked @SimonMeskens example, but it leaves the string
prototype methods available on the resulting type--which I did not like.
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 cast
operation. It may be of interest to people here.
@ForbesLindesay way to go! What do you think about https://github.com/gcanti/newtype-ts?
@qm3ster that looks pretty cool. The iso
and prism
functions are a neat approach to avoiding runtime cost for multiple opaque types in a single project. The main issue I see with it is that you've used functional terminology, instead of plain english words. You've also tightly coupled your solution to functional programming concepts like Options, which are not commonly used in JavaScript. The other small points are that you have _URI
and _A
show up as properties on your opaque values, when in fact those properties do not exist at runtime.
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.
I see how the notation might be an impedance to wider adoption.
It's positioning itself as part of a larger gcanti/fp-ts ecosystem, hence the Option
type.
@ProdigySim @SimonMeskens Nice solution but it seems to has some cons as below.
declare const symbolName:unique symbol
is long. And therefore, the user should write multi line when define opaque type alias.symbolName
in declare const symbolName:unique symbol
is necessary not. However the user should define symbolName in user's namespace.Then I tried improvement. And it seems working. Please let me know if there are problems.
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 Opaque<string,{ readonly tag:unique symbol}>
can be written in one line.
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:
[ts]
Type 'Opaque<string, typeof EmailSymbol>' is not assignable to type 'Opaque<string, typeof UserIdSymbol>'.
Type 'Opaque<string, unique symbol>' is not assignable to type 'OpaqueTag<unique symbol>'.
Types of property '[OpaqueTagSymbol]' are incompatible.
Type 'typeof EmailSymbol' is not assignable to type 'typeof UserIdSymbol'.
From @qwerty2501 's setup:
Type 'Opaque<string, { readonly tag: typeof tag; }>' is not assignable to type 'Opaque<string, { readonly tag: typeof tag; }>'. Two different types with this name exist, but they are unrelated.
I stripped the namespace from both errors to make them more equivalent. The latter is shorter, but the former explicitly calls out Email
vs UserId
.
@ProdigySim True. I think it is trade off between "easy to understand error" and "the syntax is shorter".
I checked out how Flow handles opaque types in comparison to our solutions. They have some interesting behavior.
Notably:
string & { [TagSym]: typeof UserIdSymbol }
). That is, implicit conversions TO the underlying type are allowed.{ [idx: UserId]: any }
is a valid type. This is not currently possible in typescript afaict.I put together a typescript playground link demonstrating different constructions of Opaque<T>
and their capabilities.
OpaqueTag<S>
-- no reference to underlying type, no valid casts to an underlying type.T & OpaqueTag<S>
-- matches flow behavior closely, automatic downcasts to T
T & OpqaueTag<S> | OpaqueTag<S>
-- keeps some reference to underlying type for explicit conversions, but doesn't allow implicit casting.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. Because both(@ProdigySim and me) solutions has the number of writing characters bit too much.
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>
: