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 · 190 comments

Comments

Projects
None yet
@iislucas

iislucas commented Jul 22, 2014

Proposal: support non-structural typing (new user-defined base-types). This allows programmer to have more refined types.

Uses-cases:

  1. Indexes typically come from somewhere. e.g. we have some mappings that are maintained between indexes. Because all indexes are strings, it's easy to use the an index variable (intended for one mapping) with another index variable intended for a different mapping (by mistake of course!). Because all indexes are strings, 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!):

nominal Index1 extends string {}
nominal Index2 extends string {}

nominal Table1 {
  [i :Index1] : number;
  foo?: number;
}

nominal Table2 {
  [i :Index2] : number;
  foo?: number;
  bar?: number;
}

var s1 :Index1;
var s2 :Index1;
var t1 :Table1;
var t1 :Table2;

// Assignment from base-types and basic structures probably wants to be special case 
// where no type-overloading is needed.  
s1 = 'a1';
s2 = 'a2';
table1 = {};
table2 = {
  'b2': 200
};

s2 = s1 // Error!
s2 == s1 // Error!
s2 === s1 // Error!

t1[s2] = 100; // Error;
t1[s1] = 100;

function test2(t:Table2) {
  if (s2 in t) { console.log(cool); }
  if (t2.foo) { console.log(t2.foo); }
  if (t2.bar) { console.log(t2.bar); }
}

function test1(t:Table1) {
  if (s2 in t) { console.log(cool); }  // Error!
  if (t2.foo) { console.log(t2.foo); }
  if (t2.bar) { console.log(t2.bar); } // Error!
}

test1(t1);
test2(t2);

test2(t1); // Error!
test1(t2); // Error!
@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jul 23, 2014

Member

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

+Needs Proposal

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

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas 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.

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

This comment has been minimized.

Show comment
Hide comment
@samwgoldman

samwgoldman Jul 23, 2014

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

samwgoldman commented Jul 23, 2014

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

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jul 23, 2014

Member

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.

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

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas 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?

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

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Jul 23, 2014

Member

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

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 from Support non-structural (abstract) types to Support some non-structural (nominal) type matching Jul 23, 2014

@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov 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

aleksey-bykov 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

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas 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]

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

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Jul 26, 2014

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@Steve-Fenton

Steve-Fenton 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.

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

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Jul 31, 2014

Contributor

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

👍

Contributor

basarat commented Jul 31, 2014

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

👍

@Steve-Fenton

This comment has been minimized.

Show comment
Hide comment
@Steve-Fenton

Steve-Fenton 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) {
    //...

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jul 31, 2014

Member

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

What would that mean?

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

This comment has been minimized.

Show comment
Hide comment
@Steve-Fenton

Steve-Fenton 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 :) !

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jul 31, 2014

Member

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' ?
}
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

This comment has been minimized.

Show comment
Hide comment
@Steve-Fenton

Steve-Fenton 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 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

This comment has been minimized.

Show comment
Hide comment
@Steve-Fenton

Steve-Fenton 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.

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

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Aug 1, 2014

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@ComFreek

ComFreek 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.

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.

@jonathandturner

This comment has been minimized.

Show comment
Hide comment
@jonathandturner

jonathandturner Aug 1, 2014

Contributor

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.

Contributor

jonathandturner 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

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Aug 1, 2014

Member

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.

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

This comment has been minimized.

Show comment
Hide comment
@Steve-Fenton

Steve-Fenton 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.

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

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Aug 1, 2014

Member

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).

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

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas 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

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

@jonathandturner

This comment has been minimized.

Show comment
Hide comment
@jonathandturner

jonathandturner Aug 1, 2014

Contributor

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

Contributor

jonathandturner 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

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas 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

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

@iislucas

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas Dec 30, 2014

@jonathandturner just wanted to check what you think of the example use-cases proposed in the description. Do they make sense? Happy to clarify/expand on them if that helps (and also of #202 (comment) which is a case of entry (2) in the issue description)

And also w.r.t. the suggestion of @aleksey-bykov : #202 (comment)
it seems that it could be implemented as a simple abbreviation to existing type-script without introducing any new concepts. The only optimization that would make sense to add would be for the compiler to optimize away self-referential nominal field.

I realize that I've offered to do something but not actually 100% sure what you'd like me to write - I could look through code and start pointing to how I would implement it? but maybe there's something more to do before that?

Nominal types (and new kinds of indexable types) that don't get confused between each other would be a big improvement in my opinion. They let you separate layers of abstraction and implementation details. And they are widely used in other typed functional languages too (F#, Haskell, SML/Ocaml, etc).

Thanks,

iislucas commented Dec 30, 2014

@jonathandturner just wanted to check what you think of the example use-cases proposed in the description. Do they make sense? Happy to clarify/expand on them if that helps (and also of #202 (comment) which is a case of entry (2) in the issue description)

And also w.r.t. the suggestion of @aleksey-bykov : #202 (comment)
it seems that it could be implemented as a simple abbreviation to existing type-script without introducing any new concepts. The only optimization that would make sense to add would be for the compiler to optimize away self-referential nominal field.

I realize that I've offered to do something but not actually 100% sure what you'd like me to write - I could look through code and start pointing to how I would implement it? but maybe there's something more to do before that?

Nominal types (and new kinds of indexable types) that don't get confused between each other would be a big improvement in my opinion. They let you separate layers of abstraction and implementation details. And they are widely used in other typed functional languages too (F#, Haskell, SML/Ocaml, etc).

Thanks,

@jonathandturner

This comment has been minimized.

Show comment
Hide comment
@jonathandturner

jonathandturner Dec 30, 2014

Contributor

Hi @iislucas,

For a proposal for nominal types, you'll need to show strong use cases. As listed in the design goals for TypeScript: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals, being structural is something that TypeScript is committed to. The assumption that checking is structural is carried throughout the type checker, the spec, and how it's used in practice. In short, augmenting or going away from structural is going to be uphill all the way.

Adding some additional nominal checking would need to be weighed against the benefit. For this to have a chance of success, the types of new patterns that the new nominal capabilities provide would need to enable fundamental improvements in how JavaScript is typed.

Looking at the examples in the original post, I'm not sure which JavaScript patterns this enables. Starting with that might the best place. Also, you may want to look at other new functionality, like type guards, and how this might impact that.

Contributor

jonathandturner commented Dec 30, 2014

Hi @iislucas,

For a proposal for nominal types, you'll need to show strong use cases. As listed in the design goals for TypeScript: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals, being structural is something that TypeScript is committed to. The assumption that checking is structural is carried throughout the type checker, the spec, and how it's used in practice. In short, augmenting or going away from structural is going to be uphill all the way.

Adding some additional nominal checking would need to be weighed against the benefit. For this to have a chance of success, the types of new patterns that the new nominal capabilities provide would need to enable fundamental improvements in how JavaScript is typed.

Looking at the examples in the original post, I'm not sure which JavaScript patterns this enables. Starting with that might the best place. Also, you may want to look at other new functionality, like type guards, and how this might impact that.

@iislucas

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas Jan 2, 2015

@jonathandturner I don't see why you make it seem like an exclusive choice of nominal vs structural: sometimes structural checking is what you want, sometimes nominal is. There are already a set of base nominals (string, number, etc). What I see as a good insight in typescript, and in the design goals, is that structural checking is important and will be supported. Many typed languages don't have this. It sounded to me that support for structural checking in the design goals was saying it's something TypeScript wanted to include rather than a claim about never adding an more base-types or never supporting nominal definitions. I don't see this as being a philosophical question that undermines TypeScript.

w.r.t. JavaScript patterns: it is not a JavaScript pattern that it enables. It is enables stronger levels of abstraction and more type-checking.

w.r.t. difficulty: support for nominal types is the easiest kind of extension there is to a type-system. We're not here proposing dependent types, or any fancy new computation in typing (although coming from that universe, I'm tempted... ;-) ). Just the ability to define new nominal types/interfaces.

But if I've mis-understood TypeScirpt's goals, and it is a goal to not support nominal types, then of course much stronger arguments and examples are needed.

iislucas commented Jan 2, 2015

@jonathandturner I don't see why you make it seem like an exclusive choice of nominal vs structural: sometimes structural checking is what you want, sometimes nominal is. There are already a set of base nominals (string, number, etc). What I see as a good insight in typescript, and in the design goals, is that structural checking is important and will be supported. Many typed languages don't have this. It sounded to me that support for structural checking in the design goals was saying it's something TypeScript wanted to include rather than a claim about never adding an more base-types or never supporting nominal definitions. I don't see this as being a philosophical question that undermines TypeScript.

w.r.t. JavaScript patterns: it is not a JavaScript pattern that it enables. It is enables stronger levels of abstraction and more type-checking.

w.r.t. difficulty: support for nominal types is the easiest kind of extension there is to a type-system. We're not here proposing dependent types, or any fancy new computation in typing (although coming from that universe, I'm tempted... ;-) ). Just the ability to define new nominal types/interfaces.

But if I've mis-understood TypeScirpt's goals, and it is a goal to not support nominal types, then of course much stronger arguments and examples are needed.

@jonathandturner

This comment has been minimized.

Show comment
Hide comment
@jonathandturner

jonathandturner Jan 6, 2015

Contributor

@iislucas - TypeScript's main goal is to enable large-scale JavaScript applications. It does this by staying very close to JavaScript. Most of what the type system was built for is working with JavaScript and enabling JavaScript patterns in a type-safe way.

When we were setting ourselves up for how to go about that, we went back and forth a bit on supporting both nominal and structural. At one point, we had a structural system that would tag classes with "brands" that would allow users to use them more nominally.

The more we worked with the system, though, the more it felt like going completely structural seemed closer to the spirit of JavaScript and how flexible it is in practice. Once we realized that, we went completely in the direction of structural and aren't looking to backtrack.

That's why I said it would take a lot of strong benefits to make us backtrack and re-think going fully structural.

Contributor

jonathandturner commented Jan 6, 2015

@iislucas - TypeScript's main goal is to enable large-scale JavaScript applications. It does this by staying very close to JavaScript. Most of what the type system was built for is working with JavaScript and enabling JavaScript patterns in a type-safe way.

When we were setting ourselves up for how to go about that, we went back and forth a bit on supporting both nominal and structural. At one point, we had a structural system that would tag classes with "brands" that would allow users to use them more nominally.

The more we worked with the system, though, the more it felt like going completely structural seemed closer to the spirit of JavaScript and how flexible it is in practice. Once we realized that, we went completely in the direction of structural and aren't looking to backtrack.

That's why I said it would take a lot of strong benefits to make us backtrack and re-think going fully structural.

@iislucas

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas Jan 12, 2015

@jonathandturner - thanks for the history and background. JS has generally been a difficult platform for creating large scale JS applications, and supporting this is why I and my team use it. Type-abstraction is specifically thought to help with this, and in my experience in other languages, it helps a lot. There's a long history of it in many languages, but primarily in the typed functional programming world (e.g. SML and Haskell). So if the problem is that it may be difficult to (re)implement, the thing to do is look into the details of what it would take to support it. If it is easy, then it's less of a issue. If it's hard and requires significant refactoring, then it makes sense to discuss examples and try and persuade more people - probably to do some research and point to it's role in other languages/frameworks.

The proposal of @aleksey-bykov : #202 (comment) seems easy to me. What would you say the next step in exploring this is for those of us who think it would make the language better? Is the next step a patch for typescript, or more examples using the proposed TypeScript syntax, along with errors/compilations?

iislucas commented Jan 12, 2015

@jonathandturner - thanks for the history and background. JS has generally been a difficult platform for creating large scale JS applications, and supporting this is why I and my team use it. Type-abstraction is specifically thought to help with this, and in my experience in other languages, it helps a lot. There's a long history of it in many languages, but primarily in the typed functional programming world (e.g. SML and Haskell). So if the problem is that it may be difficult to (re)implement, the thing to do is look into the details of what it would take to support it. If it is easy, then it's less of a issue. If it's hard and requires significant refactoring, then it makes sense to discuss examples and try and persuade more people - probably to do some research and point to it's role in other languages/frameworks.

The proposal of @aleksey-bykov : #202 (comment) seems easy to me. What would you say the next step in exploring this is for those of us who think it would make the language better? Is the next step a patch for typescript, or more examples using the proposed TypeScript syntax, along with errors/compilations?

@danquirk

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Jan 12, 2015

Member

The implementation is not particularly difficult (nominal classes existed before, and you can hack the same effect in today via private members). What's difficult is reconciling how nominal code would interact with structural parts of a program and all the JS libraries that are built on structural typing. As mentioned, we essentially had a system like this before (classes were nominal, interfaces and object literals were structural) and it did not mesh very well with what people already do with JS.

Member

danquirk commented Jan 12, 2015

The implementation is not particularly difficult (nominal classes existed before, and you can hack the same effect in today via private members). What's difficult is reconciling how nominal code would interact with structural parts of a program and all the JS libraries that are built on structural typing. As mentioned, we essentially had a system like this before (classes were nominal, interfaces and object literals were structural) and it did not mesh very well with what people already do with JS.

@iislucas

This comment has been minimized.

Show comment
Hide comment
@iislucas

iislucas Jan 12, 2015

Good to know the implementation is not difficult.

w.r.t. existing libraries: how about leaving all the existing JS libraries as is (using structural typing)? Nominal types can be included incrementally in new projects to catch more errors at compile time and support abstraction in newer larger software projects - it gives a nice way to move towards increased reliability in a project, e.g. to start you might introduce some nominal types for different kinds of strings to avoid mixing them up.

I would start using new nominal types in my projects pretty quickly, but I don't mind that libraries I use don't. I can always introduce an abstraction layer (e.g. a new interface) by writing a new .d.ts file for an existing library when I feel it would help me (e.g. to start with, I might do so for things like socket identifiers for chrome-app environments, see: https://developer.chrome.com/apps/sockets_tcp ).

iislucas commented Jan 12, 2015

Good to know the implementation is not difficult.

w.r.t. existing libraries: how about leaving all the existing JS libraries as is (using structural typing)? Nominal types can be included incrementally in new projects to catch more errors at compile time and support abstraction in newer larger software projects - it gives a nice way to move towards increased reliability in a project, e.g. to start you might introduce some nominal types for different kinds of strings to avoid mixing them up.

I would start using new nominal types in my projects pretty quickly, but I don't mind that libraries I use don't. I can always introduce an abstraction layer (e.g. a new interface) by writing a new .d.ts file for an existing library when I feel it would help me (e.g. to start with, I might do so for things like socket identifiers for chrome-app environments, see: https://developer.chrome.com/apps/sockets_tcp ).

@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov Jan 12, 2015

@danquirk, your major argument is that all existing libs are structural, people tend to think structural, etc

Well fine, having nominal types in addition to everything else being structural by default isn't going to hurt, is it?

The difficulties of matching structural types to nominal and shortcomings of not being able to do so 100% nicely is going to be a problem of nominal types where compromises are possible since these are the problems of minority of nominal types and doesn't affect how structural types work which are in the heart of the language.

aleksey-bykov commented Jan 12, 2015

@danquirk, your major argument is that all existing libs are structural, people tend to think structural, etc

Well fine, having nominal types in addition to everything else being structural by default isn't going to hurt, is it?

The difficulties of matching structural types to nominal and shortcomings of not being able to do so 100% nicely is going to be a problem of nominal types where compromises are possible since these are the problems of minority of nominal types and doesn't affect how structural types work which are in the heart of the language.

@wesleyolis

This comment has been minimized.

Show comment
Hide comment
@wesleyolis

wesleyolis May 31, 2018

How about introduction a new type checking modifier operator, that is implemented for type checking only. Which would allow the best of both worlds, nominal an structural typing paradigms.

As far as I know '$' is not used for specially many yet, so how about allowing $ donate use nominal type checking for the following use cases.

I would prefer to have everything swapped around, but that could break alot of backwards compatibility, mabye it could be debated to have a global compiler and localised ts file setting

Use Cases

nominal variants

assignment:

$=

call signature only:

(index :$ {index :number} => void

call signature and return:

(index :$ {index :number} $=> void

return:

(index : {index :number} $=> void

type, implies nominal should also be used were , unless explicitly cast away..

type test $= testing;

variable, implies nominal should also be used were , unless explicitly cast away..

const var1 $= 'test';
let var1 $= 'test';
readonly var1 $= 'test';

structural

assignment:

=

call signature only:

(index : {index :number} => void

call signature and return:

(index : {index :number} => void

return:

(index : {index :number} => void

type

test = testing;

variable

const var1 = 'test';
let var1 = 'test';
readonly var1 = 'test';

Casting away nominal type checking

For each nominal declare type their is an additional generated structure type name. The same behaviour as for classes, were their is a generated interface..
The $ identify must always be used in a type definition and assignments, so that it is explicitly obvious as to how type checking is being performed.

type nTypeA $= {a : string};
type nTypeB $= {a : string};
type TypeA = $ or $typeof NTypeNA

const varNa :$ nTypeStringA = {a:'test'}
const varNb :$ nTypeStringB = {a:'test'}

const varNa : typeof nTypeA = varNa; //OK allow casting
const varNb : typeof nTypeA = VarNb; //OK

const varNa : nTypeB = varNa; //OK allow casting
const varNb : nTypeA = VarNb; //OK

const varA :$ nTypeB = Na; // Error nominal type checking
const varB :$ nTypeA = Nb; // Error nominal type checking

const varNa : TypeA = varNb; //OK
const varNb : TypeA = VarNa; //OK

wesleyolis commented May 31, 2018

How about introduction a new type checking modifier operator, that is implemented for type checking only. Which would allow the best of both worlds, nominal an structural typing paradigms.

As far as I know '$' is not used for specially many yet, so how about allowing $ donate use nominal type checking for the following use cases.

I would prefer to have everything swapped around, but that could break alot of backwards compatibility, mabye it could be debated to have a global compiler and localised ts file setting

Use Cases

nominal variants

assignment:

$=

call signature only:

(index :$ {index :number} => void

call signature and return:

(index :$ {index :number} $=> void

return:

(index : {index :number} $=> void

type, implies nominal should also be used were , unless explicitly cast away..

type test $= testing;

variable, implies nominal should also be used were , unless explicitly cast away..

const var1 $= 'test';
let var1 $= 'test';
readonly var1 $= 'test';

structural

assignment:

=

call signature only:

(index : {index :number} => void

call signature and return:

(index : {index :number} => void

return:

(index : {index :number} => void

type

test = testing;

variable

const var1 = 'test';
let var1 = 'test';
readonly var1 = 'test';

Casting away nominal type checking

For each nominal declare type their is an additional generated structure type name. The same behaviour as for classes, were their is a generated interface..
The $ identify must always be used in a type definition and assignments, so that it is explicitly obvious as to how type checking is being performed.

type nTypeA $= {a : string};
type nTypeB $= {a : string};
type TypeA = $ or $typeof NTypeNA

const varNa :$ nTypeStringA = {a:'test'}
const varNb :$ nTypeStringB = {a:'test'}

const varNa : typeof nTypeA = varNa; //OK allow casting
const varNb : typeof nTypeA = VarNb; //OK

const varNa : nTypeB = varNa; //OK allow casting
const varNb : nTypeA = VarNb; //OK

const varA :$ nTypeB = Na; // Error nominal type checking
const varB :$ nTypeA = Nb; // Error nominal type checking

const varNa : TypeA = varNb; //OK
const varNb : TypeA = VarNa; //OK

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 6, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给
不关心业务逻辑而只是简单接受 string 的底层组件时,需要通过 `as string`
强转,如果该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,
结果丢失所有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型
参数(泛型声明),结果增加冗长而意义不大的泛型声明。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 6, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

而不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54
@fenduru

This comment has been minimized.

Show comment
Hide comment
@fenduru

fenduru Jun 6, 2018

I tried experimenting with modifying the TS compiler a couple months ago, but I got stuck due to a lack compiler domain knowledge. However I feel like the unique keyword gives us exactly what is needed for nominal typing. Today it is restricted to Symbols, however from my (admittedly naive) understanding of the code, it seems like this limitation is not a fundamental one (there are simply checks to ensure you're only using the keyword before a symbol).

Are there actual limitations from allowing users to write:

type NominalNumber = unique number;

fenduru commented Jun 6, 2018

I tried experimenting with modifying the TS compiler a couple months ago, but I got stuck due to a lack compiler domain knowledge. However I feel like the unique keyword gives us exactly what is needed for nominal typing. Today it is restricted to Symbols, however from my (admittedly naive) understanding of the code, it seems like this limitation is not a fundamental one (there are simply checks to ensure you're only using the keyword before a symbol).

Are there actual limitations from allowing users to write:

type NominalNumber = unique number;
@simonbuchan

This comment has been minimized.

Show comment
Hide comment
@simonbuchan

simonbuchan Jun 6, 2018

The fact that it doesn't make that much sense at runtime, if I had to guess. Symbols are special in that you get a new one every time (unless you're using .for()), and the use of unique symbol is restricted to those locations where the compiler can assume the runtime value won't change. I suppose you could make a case for id generators retuning unique number or string? But that meaning isn't really related to this issue.

In any case, being able to declare a type as being unique (ie nominal) isn't really the problem, it's that it's not clear how you should be able to use such a type, and what benefit does it give?

simonbuchan commented Jun 6, 2018

The fact that it doesn't make that much sense at runtime, if I had to guess. Symbols are special in that you get a new one every time (unless you're using .for()), and the use of unique symbol is restricted to those locations where the compiler can assume the runtime value won't change. I suppose you could make a case for id generators retuning unique number or string? But that meaning isn't really related to this issue.

In any case, being able to declare a type as being unique (ie nominal) isn't really the problem, it's that it's not clear how you should be able to use such a type, and what benefit does it give?

@fenduru

This comment has been minimized.

Show comment
Hide comment
@fenduru

fenduru Jun 6, 2018

@simonbuchan ID generators are one of the most common uses, and the one I'm particularly interested, so I'm not sure what you mean by

it's not clear how you should be able to use such a type, and what benefit does it give?

Construction of the nominal type would need to either special case variable definitions (const foo: NominalString = 'string') or users would need to explicitly cast (though that has the unfortunate downside of being unsafe). I'd personally be happy to take the downside of explicit casting, as it would allow me to "be careful" in one place, rather than all callsites.

I currently accomplish this with code along the lines of:

type NominalStringA = string & { __nominal?: unique symbol };
type NominalStringB = string & { __nominal?: unique symbol };

let a: NominalStringA = 'a';
let b: NominalStringB = 'b';
a = b; // error

This kind of works, as it at least prevents me from passing one nominal type where another is expected. It would let you pass a string to a function(x: NominalStringA) which is why assignment works, but it is at least better than nothing. All of my nominal types also need to use the same __nominal key as having that key's type conflict is how the approach works. (In practice I actually use another unique symbol as the key so __nominal doesn't show up in my editor completion)

fenduru commented Jun 6, 2018

@simonbuchan ID generators are one of the most common uses, and the one I'm particularly interested, so I'm not sure what you mean by

it's not clear how you should be able to use such a type, and what benefit does it give?

Construction of the nominal type would need to either special case variable definitions (const foo: NominalString = 'string') or users would need to explicitly cast (though that has the unfortunate downside of being unsafe). I'd personally be happy to take the downside of explicit casting, as it would allow me to "be careful" in one place, rather than all callsites.

I currently accomplish this with code along the lines of:

type NominalStringA = string & { __nominal?: unique symbol };
type NominalStringB = string & { __nominal?: unique symbol };

let a: NominalStringA = 'a';
let b: NominalStringB = 'b';
a = b; // error

This kind of works, as it at least prevents me from passing one nominal type where another is expected. It would let you pass a string to a function(x: NominalStringA) which is why assignment works, but it is at least better than nothing. All of my nominal types also need to use the same __nominal key as having that key's type conflict is how the approach works. (In practice I actually use another unique symbol as the key so __nominal doesn't show up in my editor completion)

@simonbuchan

This comment has been minimized.

Show comment
Hide comment
@simonbuchan

simonbuchan Jun 6, 2018

I mean what operations should be permitted by such a type? Can you do a + a? a.trim()? Is that still NominalStringA, or is it string? console.log(a)? Can you a.concat(a)? a.concat('foo')? Can you assign a non-literal string to a nominal string?

More importantly by far, is your answer to each of these questions going to be something generally useful, or only the specific case of ID generators? Will adding this block adding a more general solution later? In which situations does it prevent bugs? Where does it hide bugs? (e.g. casting) And so on...

This is just for the most simple case of nominal primitive types - when you get nominal object types (nominal function types?) things can get much weirder, since you have questions of how subtyping works, including variance.

simonbuchan commented Jun 6, 2018

I mean what operations should be permitted by such a type? Can you do a + a? a.trim()? Is that still NominalStringA, or is it string? console.log(a)? Can you a.concat(a)? a.concat('foo')? Can you assign a non-literal string to a nominal string?

More importantly by far, is your answer to each of these questions going to be something generally useful, or only the specific case of ID generators? Will adding this block adding a more general solution later? In which situations does it prevent bugs? Where does it hide bugs? (e.g. casting) And so on...

This is just for the most simple case of nominal primitive types - when you get nominal object types (nominal function types?) things can get much weirder, since you have questions of how subtyping works, including variance.

@spion

This comment has been minimized.

Show comment
Hide comment
@spion

spion Jun 6, 2018

FYI since nominal types would be a bit of a problem with node_module style scoped lookups, I settled with branded types

https://goo.gl/3Jjtp4

function branded<T, Brand>() {
    return class Type {
        private value: Type;
        private '__ kind': Brand;
        static toBranded(t: T) { return t as any as Type; }
        static unbrand(b: Type) { return b as any as T }
        static Type: Type;
    }
}


let OrderId = branded<string, 'OrderId'>()
type OrderId = typeof OrderId.Type;

let x = OrderId.toBranded('someUuid')
let y = 'someString';
let b1 = y == x; // generates error
let b2 = y == OrderId.unbrand(x)

let m: Map<OrderId, string> = new Map;

m.set(y, '1')
m.set(x, '2')

spion commented Jun 6, 2018

FYI since nominal types would be a bit of a problem with node_module style scoped lookups, I settled with branded types

https://goo.gl/3Jjtp4

function branded<T, Brand>() {
    return class Type {
        private value: Type;
        private '__ kind': Brand;
        static toBranded(t: T) { return t as any as Type; }
        static unbrand(b: Type) { return b as any as T }
        static Type: Type;
    }
}


let OrderId = branded<string, 'OrderId'>()
type OrderId = typeof OrderId.Type;

let x = OrderId.toBranded('someUuid')
let y = 'someString';
let b1 = y == x; // generates error
let b2 = y == OrderId.unbrand(x)

let m: Map<OrderId, string> = new Map;

m.set(y, '1')
m.set(x, '2')
@simonbuchan

This comment has been minimized.

Show comment
Hide comment
@simonbuchan

simonbuchan Jun 7, 2018

That's a box, not a brand, since it allocates a new object, but sure, you can do that too.

simonbuchan commented Jun 7, 2018

That's a box, not a brand, since it allocates a new object, but sure, you can do that too.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jun 7, 2018

Contributor

My primary use cases fall into one of two categories:

  1. I have a type whose entire structure, including typeof, is opaque and an implementation detail. This comes up a little more frequently for me than for many, since my programs are usually a little more data-driven. For one concrete example, this module packs all its data into a single 32-bit integer. I can't possibly type it except as a nominal opaque type that happens to support >/</===/etc.

  2. I return what's effectively just an ID. I do that pervasively in these two modules, with IDs orchestrated via this module. I also frequently do it in other scenarios where data handling is critical.

Contributor

isiahmeadows commented Jun 7, 2018

My primary use cases fall into one of two categories:

  1. I have a type whose entire structure, including typeof, is opaque and an implementation detail. This comes up a little more frequently for me than for many, since my programs are usually a little more data-driven. For one concrete example, this module packs all its data into a single 32-bit integer. I can't possibly type it except as a nominal opaque type that happens to support >/</===/etc.

  2. I return what's effectively just an ID. I do that pervasively in these two modules, with IDs orchestrated via this module. I also frequently do it in other scenarios where data handling is critical.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jun 7, 2018

Contributor

@simonbuchan Only during type instantiation. The toBranded and unbrand methods are the important ones here, not the constructor itself.

Contributor

isiahmeadows commented Jun 7, 2018

@simonbuchan Only during type instantiation. The toBranded and unbrand methods are the important ones here, not the constructor itself.

@simonbuchan

This comment has been minimized.

Show comment
Hide comment
@simonbuchan

simonbuchan Jun 7, 2018

Yes, but that's what a box is :) Java and C# both do autoboxing (slightly differently), but you can box a primitive (or other type) manually too.

As mentioned earlier in this thread, opaque types seem like they would satisfy many of these suggestions, if done TS should probably do that compatible with the way flow does it, but:

  • opaque types might not satisfy some of these other requests? I haven't reviewed the thread recently.
  • I don't think TS currently treats any types differently depending on context?

simonbuchan commented Jun 7, 2018

Yes, but that's what a box is :) Java and C# both do autoboxing (slightly differently), but you can box a primitive (or other type) manually too.

As mentioned earlier in this thread, opaque types seem like they would satisfy many of these suggestions, if done TS should probably do that compatible with the way flow does it, but:

  • opaque types might not satisfy some of these other requests? I haven't reviewed the thread recently.
  • I don't think TS currently treats any types differently depending on context?
@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jun 7, 2018

Contributor

@spion @simonbuchan I made it a bit lower-memory, so it doesn't require any runtime allocation at all (it's trivially inlinable).

Edit: muffed up the link.

Contributor

isiahmeadows commented Jun 7, 2018

@spion @simonbuchan I made it a bit lower-memory, so it doesn't require any runtime allocation at all (it's trivially inlinable).

Edit: muffed up the link.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jun 7, 2018

Contributor

@simonbuchan There's no "autoboxing" involved. Type instantiation is analogous to class instantiation here, not value instantiation.

I do want something similar to what Flow has, either way. As long as I can unsafely cast it into its component type (or at least safely cast it within the same file), I'm happy.

Contributor

isiahmeadows commented Jun 7, 2018

@simonbuchan There's no "autoboxing" involved. Type instantiation is analogous to class instantiation here, not value instantiation.

I do want something similar to what Flow has, either way. As long as I can unsafely cast it into its component type (or at least safely cast it within the same file), I'm happy.

@simonbuchan

This comment has been minimized.

Show comment
Hide comment
@simonbuchan

simonbuchan Jun 7, 2018

Yes, that's now no longer boxing, but your types are a bit messed up, claiming it has a T property with the value, which it doesn't. There's many many many many, suggestions for branding types in this thread!

simonbuchan commented Jun 7, 2018

Yes, that's now no longer boxing, but your types are a bit messed up, claiming it has a T property with the value, which it doesn't. There's many many many many, suggestions for branding types in this thread!

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jun 7, 2018

Contributor

@simonbuchan I'm not afraid to lie at the type level for better static verification, and I've done plenty of times in TypeScript. I've yet to see a workaround here for adding sufficiently "opaque" types that doesn't require lying about types to some extent. This even includes @spion's workaround which fails to implement static type: Type correctly.

As long as you know not to actually try to use it at the value level, you should be good. (I've yet to run into a case where it became a real problem, and it's not especially common to want to store values with their brands.)

Contributor

isiahmeadows commented Jun 7, 2018

@simonbuchan I'm not afraid to lie at the type level for better static verification, and I've done plenty of times in TypeScript. I've yet to see a workaround here for adding sufficiently "opaque" types that doesn't require lying about types to some extent. This even includes @spion's workaround which fails to implement static type: Type correctly.

As long as you know not to actually try to use it at the value level, you should be good. (I've yet to run into a case where it became a real problem, and it's not especially common to want to store values with their brands.)

@simonbuchan

This comment has been minimized.

Show comment
Hide comment
@simonbuchan

simonbuchan Jun 7, 2018

Sure, but since you're casting anyway, you don't need the value property, since it doesn't actually use it.

simonbuchan commented Jun 7, 2018

Sure, but since you're casting anyway, you don't need the value property, since it doesn't actually use it.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
Contributor

isiahmeadows commented Jun 7, 2018

@spion

This comment has been minimized.

Show comment
Hide comment
@spion

spion Jun 7, 2018

If you look at it carefully, it is in fact a brand and not a box. The class stuff is there mainly to hide the machinery using private (e.g. the type has no visible .value property)

spion commented Jun 7, 2018

If you look at it carefully, it is in fact a brand and not a box. The class stuff is there mainly to hide the machinery using private (e.g. the type has no visible .value property)

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 15, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

而不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 24, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

而不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 25, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

而不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 27, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

而不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 27, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)
 4.可以直接使用在对象 index 的位置

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3、4。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

不满足4,有时通过把 id 作为 key,构建简易的字典对象时,就不得不麻烦地
在 [] 里写 `as string`。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3、4。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54

chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 28, 2018

refactor(typings): 改善和纠正各种 id 类型的性质
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)
 4.可以直接使用在对象 index 的位置

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3、4。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

不满足4,有时通过把 id 作为 key,构建简易的字典对象时,就不得不麻烦地
在 [] 里写 `as string`。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3、4。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - Microsoft/TypeScript#15807
 - Microsoft/TypeScript#4895
 - Microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54
@nafg

This comment has been minimized.

Show comment
Hide comment
@nafg

nafg commented Jul 11, 2018

cf. scala opaque type aliases, I think

https://docs.scala-lang.org/sips/opaque-types.html

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jul 11, 2018

Contributor

@nafg I like that concept. That's a good idea: internally treat them as aliases, but export them as opaque nominal types.

Contributor

isiahmeadows commented Jul 11, 2018

@nafg I like that concept. That's a good idea: internally treat them as aliases, but export them as opaque nominal types.

@mohsen1

This comment has been minimized.

Show comment
Hide comment
@mohsen1

mohsen1 Jul 12, 2018

Contributor

why instead of marking type declarations nominal you're not considering making type assignment and assertions nominal with a new syntax?

type A = {}
type B = {}

var a := A = {};
var b := B = {};

a = b; // Error

var aa : A = {};
var bb : B = {};
aa = bb // OK

Alternative syntax:

var a : nominal A = {}
Contributor

mohsen1 commented Jul 12, 2018

why instead of marking type declarations nominal you're not considering making type assignment and assertions nominal with a new syntax?

type A = {}
type B = {}

var a := A = {};
var b := B = {};

a = b; // Error

var aa : A = {};
var bb : B = {};
aa = bb // OK

Alternative syntax:

var a : nominal A = {}
@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jul 12, 2018

Contributor

@mohsen1 The whole point of nominal typing is to force users to stray away from relying on implementation details, not to enable them to avoid it by convention (which is already possible by just not accessing certain properties).

Contributor

isiahmeadows commented Jul 12, 2018

@mohsen1 The whole point of nominal typing is to force users to stray away from relying on implementation details, not to enable them to avoid it by convention (which is already possible by just not accessing certain properties).

@ForbesLindesay

This comment has been minimized.

Show comment
Hide comment
@ForbesLindesay

ForbesLindesay Jul 13, 2018

I recently built https://github.com/ForbesLindesay/opaque-types, which may be of interest to people here. It's a transpiler that generates opaque types (using various hacks involving symbols and declared classes with private properties) from simple type aliases marked with comments/annotations.

ForbesLindesay commented Jul 13, 2018

I recently built https://github.com/ForbesLindesay/opaque-types, which may be of interest to people here. It's a transpiler that generates opaque types (using various hacks involving symbols and declared classes with private properties) from simple type aliases marked with comments/annotations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment