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

Support some non-structural (nominal) type matching #202

Open
iislucas opened this issue Jul 22, 2014 · 422 comments
Open

Support some non-structural (nominal) type matching #202

iislucas opened this issue Jul 22, 2014 · 422 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@iislucas
Copy link

iislucas commented Jul 22, 2014

Proposal: support non-structural typing (e.g. new user-defined base-types, or some form of basic nominal typing). This allows programmer to have more refined types supporting frequently used idioms such as:

  1. Indexes that come from different tables. Because all indexes are strings (or numbers), it's easy to use the an index variable (intended for one table) with another index variable intended for a different table. Because indexes are the same type, no error is given. If we have abstract index classes this would be fixed.

  2. Certain classes of functions (e.g. callbacks) can be important to be distinguished even though they have the same type. e.g. "() => void" often captures a side-effect producing function. Sometimes you want to control which ones are put into an event handler. Currently there's no way to type-check them.

  3. Consider having 2 different interfaces that have different optional parameters but the same required one. In typescript you will not get a compiler error when you provide one but need the other. Sometimes this is ok, but very often this is very not ok and you would love to have a compiler error rather than be confused at run-time.

Proposal (with all type-Error-lines removed!):

// Define FooTable and FooIndex
nominal FooIndex = string;  // Proposed new kind of nominal declaration.
interface FooTable {
  [i: FooIndex]: { foo: number };
}
let s1: FooIndex;
let t1: FooTable;

// Define BarTable and BarIndex
nominal BarIndex = string; // Proposed new kind of nominal declaration.
interface BarTable {
  [i: BarIndex]: { bar: string };
}
let s2: BarIndex;
let t2: BarTable;

// For assignment from base-types and basic structures: no type-overloading is needed.
s1 = 'foo1';
t1 = {};
t1[s1] = { foo: 1 };

s2 = 'bar1';
t2 = { 'bar1': { bar: 'barbar' }};

console.log(s2 = s1); // Proposed to be type error.
console.log(s2 == s1); // Proposed to be type error.
console.log(s2 === s1); // Proposed to be type error.

t1[s2].foo = 100; // Gives a runtime error. Proposed to be type error.
t1[s1].foo = 100;

function BadFooTest(t: FooTable) {
  if (s2 in t) {  // Proposed to be type error.
    console.log('cool');
    console.log(t[s2].foo); // Proposed to be type error.
  }
}

function GoodBarTest(t: BarTable) {
  if (s2 in t) {
    console.log('cool');
    console.log(t[s2].bar);
  }
}

BadFooTest(t1); // Gives runtime error;
BadFooTest(t2); // No runtime error, Proposed to be type error.
GoodBarTest(t1); // Gives runtime error; Proposed to be type error.
GoodBarTest(t2);
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 23, 2014

Is there a better keyword here than "abstract" ? People are going to confuse it with "abstract class".

+Needs Proposal

@iislucas
Copy link
Author

iislucas commented Jul 23, 2014

w.r.t. Needs Proposal: do you mean how to implement it? For compilation to JS, nothing needs to be changed. But would need internal identifiers for new types being introduced and an extra check at assignment.

@samwgoldman
Copy link

samwgoldman commented Jul 23, 2014

Regarding a name, what about "nominal" types? Seems pretty common in literature.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 23, 2014

We're still writing up the exact guidelines on suggestions, but basically "Needs Proposal" means that we're looking for someone to write up a detailed formal explanation of what the suggestion means so that it can be more accurately evaluated.

In this case, that would mean a description of how these types would fit in to all the various type algorithms in the spec, defining in precise language any "special case" things, listing motivating examples, and writing out error and non-error cases for each new or modified rule.

@iislucas
Copy link
Author

iislucas commented Jul 23, 2014

@RyanCavanaugh Thanks! Not sure I have time for that this evening :) but if the idea would be seriously considered I can either do it, to get someone on my team to do so. Would you want an implementation also? Or would a clear design proposal suffice?

@danquirk
Copy link
Member

danquirk commented Jul 23, 2014

@iislucas no implementation is necessary for "Needs Proposal" issues, just something on the more formal side like Ryan described. No rush ;)

@iislucas iislucas changed the title Support non-structural (abstract) types Support some non-structural (nominal) type matching Jul 23, 2014
@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jul 25, 2014

There is a workaround that I use a lot in my code to get nominal typing, consider:

interface NominalA {
   'I am a nominal type A, nobody can match me to anything I am not': NominalA;
    value: number;
}

interface NominalB {
   'I am a nominal type B, mostly like A but yet quite different': NominalB;
   value: number;
}

// using <any> on constructing instances of such nominal types is the price you have to pay
// I use special constructor functions that do casting internally producing a nominal object to avoid doing it everywhere
var a : NominalA = <any>  { value: 1 };
var b : NominalB = <any>  { value: 2 };

a = b; // <-- problema

@iislucas
Copy link
Author

iislucas commented Jul 26, 2014

Neat trick! Slight optimization, you can use:

var a = <NominalA>  { value: 1 };
var b = <NominalB>  { value: 2 };

(Slightly nicer/safer looking syntax)
[Shame it doesn't work for creating distinct types for string that you want to be indexable]

@basarat
Copy link
Contributor

basarat commented Jul 26, 2014

@Aleksey-Bykov nice trick. We have nominal Id types on the server (c#) that are serialized as strings (and we like this serialization). We've wondered of a good way to do that without it all being string on the client. We haven't seen bugs around this on the client but we still would have liked that safety. Based on your code the following looks promising (all interfaces will be codegened):

// FOO 
interface FooId{
    'FooId':string; // To prevent type errors
}
interface String{   // To ease client side assignment from string
    'FooId':string;
}
// BAR
interface BarId{
    'BarId':string; // To prevent type errors
}
interface String{   // To ease client side assignment from string
    'BarId':string;
}


var fooId: FooId;
var barId: BarId;

// Safety!
fooId = barId; // error 
barId = fooId; // error 
fooId = <FooId>barId; // error 
barId = <BarId>fooId; // error

// client side assignment. Think of it as "new Id"
fooId = <FooId>'foo';
barId = <BarId>'bar';

// If you need the base string 
// (for generic code that might operate on base identity)
var str:string;
str = <string>fooId;
str = <string>barId;  

@Steve-Fenton
Copy link

Steve-Fenton commented Jul 31, 2014

We could look at an implementation that largely left the syntax untouched: perhaps we could add a single new keyword that switches on "nominality" for a given interface. That would leave the TypeScript syntax largely unchanged and familiar.

class Customer {
    lovesUs: boolean;
}

named class Client {
    lovesUs: boolean;
}

function exampleA(customer: Customer) {

}

function exampleB(customer: Client) {

}

var customer = new Customer();
var client = new Client();

exampleA(customer);
exampleA(client);

exampleB(customer); // <- Not allowed
exampleB(client);

So you can use a Client where a Customer is needed, but not vice versa.

You could fix the error in this example by having Customer extend Client, or by using the correct named type - at which point the error goes away.

You could use the "named" switch on classes and interfaces.

@basarat
Copy link
Contributor

basarat commented Jul 31, 2014

You could use the "named" switch on classes and interfaces.

👍

@Steve-Fenton
Copy link

Steve-Fenton commented Jul 31, 2014

You could also use it to make a type nominal in a specific context, even if the type was not marked as nominal:

function getById(id: named CustomerId) {
    //...

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 31, 2014

You could also use it to make a type nominal in a specific context

What would that mean?

@Steve-Fenton
Copy link

Steve-Fenton commented Jul 31, 2014

When used as part of a type annotation, it would tell the compiler to compare types nominally, rather than structurally - so you could decide when it is important for the exact type, and when it isn't.

It would be equivalent to specifying it on the class or interface, but would allow you to create a "structural" interface that in your specific case is treated as "nominal".

Or, I have jumped the shark :) !

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 31, 2014

An example of an error (or non-error) would be nice. I can't figure out how you'd even use this thing

interface CustomerId { name: string }
interface OrderId { name: string }
function getById(id: named CustomerId) {
    //...
}
var x = {name: 'bob'};
getById(x); // Error, x is not the nominal 'named CustomerId' ?

function doubleNamed1(a: named CustomerId, b: named OrderId) {
    a = b; // Legal? Not legal?
}
function doubleNamed2(a: named CustomerId, b: named CustomerId) {
    a = b; // Legal? Not legal?
}
function namedAnon(x: named { name: string }) {
     // What does this even mean? How would I make a value compatible with 'x' ?
}

@Steve-Fenton
Copy link

Steve-Fenton commented Jul 31, 2014

This is why I'm not a language designer :)

I've shown in the example below that the keyword applies for the scope of the variable. If you make a parameter nominal, it is nominal for the whole function.

interface CustomerId { name: string }
interface OrderId { name: string }
function getById(id: named CustomerId) {
    //...
}
var x = {name: 'bob'};
getById(x); // Error, x is not the nominal 'named CustomerId'

function doubleNamed1(a: named CustomerId, b: named OrderId) {
    a = b; // Not legal, a is considered to be a nominal type
}
function doubleNamed2(a: named CustomerId, b: named CustomerId) {
    a = b; // Legal, a is compared nominally to b and they are the same type
}
function singleNamed1(a: named CustomerId, b: CustomerId) {
    a = b; // Legal, a is compared nominally to b and they are the same type
}
function namedAnon(x: named { name: string }) {
     // Compiler error - the "named" keyword can only be applied to interfaces and classes
}

@Steve-Fenton
Copy link

Steve-Fenton commented Jul 31, 2014

I admit that the notion of marking an item as nominal temporarily as per these recent examples may have been a little flippant - in the process of thinking through the implications of the feature I'm happy to accept it may be a terrible idea.

I'd hate for that to affect the much more straightforward idea marking a class or interface as nominal at the point it is defined.

@basarat
Copy link
Contributor

basarat commented Aug 1, 2014

Will need an inline creation syntax. Suggestion, a named assertion:

var x = <named CustomerId>{name: 'bob'};  // x is now named `CustomerId`
getById(x); // okay

Perhaps there can be a better one.

@ComFreek
Copy link

ComFreek commented Aug 1, 2014

I wonder what the use cases for library developers are to not request nominal type checking via name.

Wouldn't you always be on the safe side if you use name by default? If the caller does have the right type, all is fine. If he doesn't, he must convert it (e.g. using the syntax @basarat suggested). If the conversion works, but doesn't work as expected, it's the user's fault and not the library developer's fault.

Maybe the whole problem is the duck typing system itself. But that's one problem TypeScript shouldn't solve, I suppose.

@jntrnr
Copy link
Contributor

jntrnr commented Aug 1, 2014

Not to sound like a sour puss, but being a structural type system is a fork in the road early on in how the type system works. We intentionally went structural to fit in better with JavaScript and then added layer upon layer of type system machinery on top of it that assumes things are structural. To pull up the floor boards and rethink that is a ton of work, and I'm not clear on how it adds enough value to pay for itself.

It's worth noting, too, the complexity it adds in terms of usability. Now people would always need to think about "is this type going to be used nominally or structurally?" Like Ryan shows, once you mix in patterns that are common in TypeScript the story gets murky.

It may have been mentioned already, but a good article for rules of thumb on new features is this one: http://blogs.msdn.com/b/ericgu/archive/2004/01/12/57985.aspx

The gist is that assume every new feature starts at -100 points and has to pay for itself in terms of added benefit. Something that causes a deep rethink of the type system is probably an order of magnitude worse. Not to say it's impossible. Rather, it's highly unlikely a feature could be worth so much.

@danquirk
Copy link
Member

danquirk commented Aug 1, 2014

Agree with Jonathan here. I would have to see an extremely thorough proposal with some large code examples that prove this doesn't quickly become unmanageable. I have a hard time imagining how you could effectively use this modifier in a restricted set of circumstances without it leaking everywhere and ending up with you needing to use it on every type in your program (or giving up entirely on things like object literals). At that point you're talking about a different language that is basically incompatible with JavaScript.

Remember that nominal systems come with pain too and have patterns they don't represent as well. The trade off to enable those patterns with a structural system is the occasional overlap of structurally equal but conceptually different types.

@Steve-Fenton
Copy link

Steve-Fenton commented Aug 1, 2014

So the most common use case for this (that I can think of) is type-safe ids. Currently, you can create these in TypeScript by adding a private member to a class (or a crazy identifier on an interface, although that only reduces the chance, whereas the private member trick works as expected).

You have already made the decision that you want a nominal type when you create a type safe id class, because that is the purpose of such a class (and is the reason you aren't simply using number).

So my question is as follows, this code does what a lot of people want:

    class ExampleId {
        constructor(public value: number){}
        private notused: string;
    }

i.e. you cannot create another type that will satisfy this structure, because of the private member...

  1. Would it be possible to formalise this behaviour with a keyword so the private member isn't needed?
  2. Would it be possible to get this behaviour for interfaces?

The first of these two questions would probably cover 80% of the use cases. The second would allow similar cases and would be very useful from a .d.ts perspective.

This limits the feature to the creation of types that cannot be matched, which is already possible as described and for classes simply moves a "magic fix" into a more deliberate keyword.

I would be happy to write up something for this.

@danquirk
Copy link
Member

danquirk commented Aug 1, 2014

Certainly feel free to try to write up something more complete that can be evaluated, although I will be honest and say the chances of us taking a change like seem quite slim to me.

Another data point to consider is that TypeScript classes had this behavior by default for some time (ie always behaved as a nominal type) and it was just very incongruous with the rest of the type system and ways in which object types were used. Obviously the ability to turn nominal on/off is quite different from always on but something to consider nonetheless. Also, as you note this pattern does allow some amount of nominal typing today, so it would be interesting to see if there are any codebases that have used this intermixing to a non-trivial degree (in a way that isn't just all nominal all the time).

@iislucas
Copy link
Author

iislucas commented Aug 1, 2014

Note: lets not mix up the baby and bathwater here: the proposal in this
issue is not a nominal keyword for any type, but to support a specific
interface declaration of a nominal type. Nominal types are easy get right,
and pretty well understood to provide value; while a 'sticky' nominal type
annotation is tricky to do right. I'd suggest moving discussion of a
anywhere nominal type-tag to a different issue so as not to confuse the
two.

On Fri, Aug 1, 2014 at 4:37 PM, Dan Quirk notifications@github.com wrote:

Certainly feel free to try to write up something more complete that can be
evaluated, although I will be honest and say the chances of us taking a
change like seem quite slim to me.

Another data point to consider is that TypeScript classes had this
behavior by default for some time (ie always behaved as a nominal type) and
it was just very incongruous with the rest of the type system and ways in
which object types were used. Obviously the ability to turn nominal on/off
is quite different from always on but something to consider nonetheless.
Also, as you note this pattern does allow some amount of nominal typing
today, so it would be interesting to see if there are any codebases that
have used this intermixing to a non-trivial degree (in a way that isn't
just all nominal all the time).


Reply to this email directly or view it on GitHub
#202 (comment)
.

Lucas Dixon | Google Ideas

@jntrnr
Copy link
Contributor

jntrnr commented Aug 1, 2014

@iislucas - as mentioned earlier, structural and nominal are fundamental choices in the type system. Any time you rethink part of the fundamental design choices, you need to understand the full impact. Even if it seems to be isolated to a small set of scenarios.

The best way to full understand the impact is to have a more complete suggestion. I wouldn't confuse @danquirk's response as throwing the baby with the bathwater, but instead as the minimal amount of work any proposal would need that touches a fundamental design choice.

@iislucas
Copy link
Author

iislucas commented Aug 1, 2014

I agree that a fully proposal is a good idea, and I'll do that. I worked a
long time in type-systems, so I'm pretty confident in my understanding of
whats involved here. But there are wildly different things being suggested.
So probably good to put each one into it's own discussion :)

On Fri, Aug 1, 2014 at 5:17 PM, Jonathan Turner notifications@github.com
wrote:

@iislucas https://github.com/iislucas - as mentioned earlier,
structural and nominal are fundamental choices in the type system. Any time
you rethink part of the fundamental design choices, you need to understand
the full impact. Even if it seems to be isolated to a small set of
scenarios.

The best way to full understand the impact is to have a more complete
suggestion. I wouldn't confuse @danquirk https://github.com/danquirk's
response as throwing the baby with the bathwater, but instead as the
minimal amount of work any proposal would need that touches a fundamental
design choice.


Reply to this email directly or view it on GitHub
#202 (comment)
.

Lucas Dixon | Google Ideas

@lu4
Copy link

lu4 commented Nov 5, 2021

You can do something like (This is updated answer):

export type Miles = number & { readonly '': unique symbol };
export type Kilograms = number & { readonly '': unique symbol }
export type MilesPerKilogram = number & { readonly '': unique symbol }

var a: Miles = 3; // error
var b: Kilograms = 3; // error
var c: MilesPerKilogram = x / y; // error

a = b; // error

var x: Miles = 3 as Miles; // ok
var y: Kilograms = 3 as Kilograms; // ok
var u: MilesPerKilogram = x / y as MilesPerKilogram; // ok

See code

Be careful though as you are not allowed to wrap nominal type declaration into a separate type as in such case a unique symbol will be shared across all type usages which would essentially share type "nominality" and thus beat it's original purpose.

export type Nominal<T> = T & { readonly '': unique symbol };

export type Miles = Nominal<number>;
export type Kilograms = Nominal<number>;
export type MilesPerKilogram = Nominal<number>;

var a: Miles = 3; // error
var b: Kilograms = 3; // error
var c: MilesPerKilogram = x / y; // error

var x: Miles = 3 as Miles; // ok
var y: Kilograms = 3 as Kilograms; // ok
var u: MilesPerKilogram = x / y as MilesPerKilogram; // ok

a = b; // ok, but it should be error! <========================= This is due to declaration of Nominal<T>

See code

The suggested solution is still far from being perfect but covered most of my use-cases I had chance to face

@Janpot
Copy link

Janpot commented Nov 5, 2021

I usually do a variation on that:

declare const t: unique symbol;
export type Nominal<T> = T & { readonly [t]: unique symbol };

edit

Actually, after reading the comments below, I checked my actual code again and I'm doing something different indeed 🙂. Still allows assignment of primitives though.

declare const t: unique symbol;
export type Nominal<T, U> = T & { readonly [t]: U };

type Miles = Nominal<number, 'Miles'>
type Kilometers = Nominal<number, 'Kilometers'>

@evelant
Copy link

evelant commented Nov 5, 2021

@Janpot @lu4 doesn't that allow assigning any nominal type to any other nominal type that structurally matches? For example:

type Nominal<T> = T & { readonly [t]: unique symbol }

type Miles = Nominal<number>
type Kilometers = Nominal<number>

let distanceInMiles: Miles = 3 as Miles
let distanceInKm = 4.83 as Kilometers

//uhoh, TS lets us assign kilometers to miles, that doesn't make sense
distanceInMiles = distanceInKm

if (distanceInMiles <= 3) {
    //Uhoh, the probe is going to crash into mars because we were allowed to accidentally assign KM to miles
   //which actually happened once https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
    adjustOrbitalDescentTrajectory()
}

edit:
The solution proposed by @bendman works as expected

type Nominal<Name extends string, Tag, Type = {}> = Type & {
    readonly __tuid__: { name: Name; tag: Tag }
}

type Miles = Nominal<"Miles", { readonly id: unique symbol }, number>
type Kilometers = Nominal<"Kilometers", { readonly id: unique symbol }, number>

let distanceInMiles: Miles = 3 as Miles
let distanceInKm = 4.83 as Kilometers

//this is not assignable, good!
distanceInMiles = distanceInKm
//Type 'Kilometers' is not assignable to type 'Miles'.
//  Type 'Kilometers' is not assignable to type '{ readonly __tuid__: { name: "Miles"; tag: { readonly id: unique symbol; }; //}; }'.
//    The types of '__tuid__.name' are incompatible between these types.
//      Type '"Kilometers"' is not assignable to type '"Miles"'.ts(2322)

edit2:

That solution does however allow comparison of nominal types with primitive types without an error which could cause bugs

type Miles = Nominal<"Miles", { readonly id: unique symbol }, number>
type Kilometers = Nominal<"Kilometers", { readonly id: unique symbol }, number>

let distanceInKm: Kilometers = 4.83 as Kilometers
let distanceInMiles: Miles = 3 as Miles

//TS doesn't allow comparison of two nominal types, "Miles and Kilometers have no overlap"
if (distanceInMiles === distanceInKm) {
}

//TS allows a comparison of number to Miles, but not assignment
// it would be good to have an error here as it is likely a bug
let amountOfFuel = 12452
if (distanceInMiles === amountOfFuel) {
}

@lu4
Copy link

lu4 commented Nov 5, 2021

@AndrewMorsillo Pardon me, I've did some optimizations in the process of preparing an answer and haven't checked for correctness. The proper version of code would look like this:

export type Miles = number & { readonly '': unique symbol };
export type Kilograms = number & { readonly '': unique symbol };
export type MilesPerKilogram = number & { readonly '': unique symbol };

var a: Miles = 3; // error
var b: Kilograms = 3; // error
var c: MilesPerKilogram = x / y; // error

a = b; // error

var x: Miles = 3 as Miles; // ok
var y: Kilograms = 3 as Kilograms; // ok
var u: MilesPerKilogram = x / y as MilesPerKilogram; // ok

See code

I will update original answer to reduce confusion

@evelant
Copy link

evelant commented Nov 5, 2021

@lu4 That certainly works better but it still allows comparison of nominal types to primitives, for example:

let distance: Miles = 3 as Miles; // ok
let fuelWeight = 3583
//uhoh, probably not what was intended but no error when comparing number to Miles!
if(distance < fuelWeight){
}

I think maybe this technique suggested above is the best we can get right now but still not perfect

interface NominalNumber<T extends string> extends Number {
  readonly __typeName: T;
}

type Miles = NominalNumber<"miles">
type Kilograms = NominalNumber<"kilograms">

//Error: Type 'number' is not assignable to type 'Miles'
let x: Miles = 3
//Error: Conversion of type 'number' to type 'Miles' may be a mistake because neither type sufficiently overlaps with the other.
let y = 3 as Miles

//OK
let distance = 3 as unknown as Miles
let weight = 10 as unknown as Kilograms

//Error: Type 'Kilograms' is not assignable to type 'Miles'
distance = weight

//Error: This condition will always return 'false' since the types 'Miles' and 'Kilograms' have no overlap.
if(distance === weight){}

let durationSeconds = 3
//Better! No accidental comparison of raw numbers to Miles
//Error: This condition will always return 'false' since the types 'Miles' and 'number' have no overlap.
if(distance === durationSeconds){}

//But still not great because we can make asssignment mistakes due to "as unknown"
//No error here. Would be better if TS would ensure we at least have a number when casting to Miles.
let badDistance = "uhoh" as unknown as Miles
//Looks like this is unavoidable with this technique until as have more fine grained type assertion operator
//https://github.com/microsoft/TypeScript/issues/7481

playground link

@lu4
Copy link

lu4 commented Nov 5, 2021

Ahh, yes that is unfortunate truth... But hey, you are free to choose from several solutions. There's also this book https://basarat.gitbook.io/typescript/main-1/nominaltyping where Nominal typing is discussed they have several other
approaches that may be useful for you. In my case I was looking for more or less clean (in terms of declaration and in terms of IDE list of suggestions pollution) way of declaring nominal types and it suited me. It was not related to arithmetic code but rather it became handy when declaring database ID fields. Compiler happily shows errors in places where extreme precaution is required.

@lu4
Copy link

lu4 commented Nov 5, 2021

Giving more thought to it @AndrewMorsillo why wouldn't it be possible to compare or perform other mathematical operations on incompatible types. You are free to divide miles over gallons in which case you should get a Miles Per Gallon. Like with taking a square root of a negative number it depends on the context where mathematical operation is used. Same thinking goes to comparison operators we actually can compare mosquito to an elephant in some context, say size, but not in some other. Thus one can impose definitive restrictions only on assignment... This is where one can be absolutely sure about the check in any context.

@evelant
Copy link

evelant commented Jan 30, 2022

@lu4 It certainly makes sense to perform operations on incompatible nominal types but IMO in order to prevent bugs and have safety it's best to be forced to be explicit about any operations involving differing types.

It makes sense to operate on differing types like Miles / Gallons but most of the time I think it only makes sense on a per-context basis. For example const speed = Miles / Gallons would be a bug since Miles/Gallons clearly isn't a representation of speed as intended -- if you allow these operations without warning it's easy to introduce bugs.

When you can implicitly do operations on types with similar data but different or contextually important meanings it's easy to end up crashing into the surface of mars because you accidentally compared Miles to Kilometers without realizing it.

@lu4
Copy link

lu4 commented Jan 30, 2022

I don't think typescript was ever used for any Mars missions so I think we're on the safe side here :) There is no one size that fits all. If you have cases where you need comparison not to work for two different brands of numbers please use the approach that helps you achieve your goal. But the stricter the type system the harder it becomes to use it so you are the one who chooses the balance there. One extreme example from my university days in this regard is when we spent nearly a semester on Category theory just to define a plus operation it was very precise definition of plus but we've never actually got much chance to use it for adding anything...

@evelant
Copy link

evelant commented Jan 30, 2022

I wasn't intending to say that TS should force strict nominal typing on anybody. I'd just like for TS to support it in an ergonomic way for people who would like to use it.

Doing implicit mathematical operations on variables that have different meanings but the same type is already fully supported, just use the number type! For cases where you want nominal typing however I think allowing implicit comparison and operations somewhat defeats the purpose.

@michaelKurowski
Copy link

michaelKurowski commented May 10, 2022

It would be good to have some sort of distinct type casting for nominal types i.e.
myVariable is MyNominalType instead of using as. My reasoning here is that using as is very often a workaround while I'd expect typecasting in terms of nominal types to be a standard. Having a separate syntax wouldn't introduce noise during CR process while one would likely like to pay extra attention while seeing as being used in contrast to usual TS operations.

@shicks
Copy link
Contributor

shicks commented May 27, 2022

There's some good discussion upthread around opaque/unique nominal types, which were prototyped a bit in #33038 (though I like the new number syntax a little better), but there was a parallel experiment in #33290 that explored "structural" brands, which generalizes to the opaque/unique case by making a new (actual) unique symbol for the structural parameter.

The latter thread was just locked, though it's not clear to me that it's "just" an implementation sketch for nominal types. As far as I'm aware, the experiment didn't end in any sort of failure, it just fizzled out - so potentially it's a viable approach to solving this issue. I.e.,

declare const milesTag: unique symbol;
type Miles = number & tag (typeof milesTag);

This approach has the advantage that the structural tag allows the same nominal type to be defined in multiple places, when that may be desirable (i.e. for API compatibility), though that's opt-in (the above example would be truly unique and not forgeable elsewhere).

It looks like the TS maintainers aren't paying much attention to this issue, but I'm wondering if there's any interest in picking this back up again?

@jsoldi
Copy link

jsoldi commented Jul 1, 2022

Wouldn't this be a simple way to avoid adding fake properties?

declare const isFinite: unique symbol
declare const isInteger: unique symbol

export type finite = number & { readonly [isFinite]: true }
export type integer = finite & { readonly [isInteger]: true }

export const numberIsFinite = (n: unknown): n is finite => typeof n === 'number' && Number.isFinite(n)
export const numberIsInteger = (n: unknown): n is integer => typeof n === 'number' && Number.isInteger(n)

Since isFinite and isInteger are not exported, they'll only be usable from inside the module. Is there a downside to doing this, other than being a little more verbose?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.