-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support some non-structural (nominal) type matching #202
Comments
Is there a better keyword here than "abstract" ? People are going to confuse it with "abstract class". +Needs Proposal |
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. |
Regarding a name, what about "nominal" types? Seems pretty common in literature. |
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. |
@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 no implementation is necessary for "Needs Proposal" issues, just something on the more formal side like Ryan described. No rush ;) |
There is a workaround that I use a lot in my code to get nominal typing, consider:
|
Neat trick! Slight optimization, you can use:
(Slightly nicer/safer looking syntax) |
@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 // 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; |
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.
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. |
👍 |
You could also use it to make a type nominal in a specific context, even if the type was not marked as nominal:
|
What would that mean? |
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 :) ! |
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' ?
} |
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.
|
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. |
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. |
I wonder what the use cases for library developers are to not request nominal type checking via Wouldn't you always be on the safe side if you use Maybe the whole problem is the duck typing system itself. But that's one problem TypeScript shouldn't solve, I suppose. |
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. |
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. |
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 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...
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 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. |
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). |
Note: lets not mix up the baby and bathwater here: the proposal in this On Fri, Aug 1, 2014 at 4:37 PM, Dan Quirk [email protected] wrote:
Lucas Dixon | Google Ideas |
@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. |
I agree that a fully proposal is a good idea, and I'll do that. I worked a On Fri, Aug 1, 2014 at 5:17 PM, Jonathan Turner [email protected]
Lucas Dixon | Google Ideas |
I wasn’t sure of the best way to approach the typings of the modules (`Rest` etc). They should be opaque to the user (they shouldn’t be interacting with them directly and we want be free to change their interface in the future). There is an open issue to add support for opaque types to TypeScript [1], and people have suggested various sorts of ways of approximating them, which revolve around the use of `unique symbol` declarations. However, I don’t fully understand these solutions and so thought it best not to include them in our public API. So, for now, let’s just use `unknown`, the same way as we do for `CipherParams.key`. Resolves #1442. [1] microsoft/TypeScript#202
I wasn’t sure of the best way to approach the typings of the modules (`Rest` etc). They should be opaque to the user (they shouldn’t be interacting with them directly and we want be free to change their interface in the future). There is an open issue to add support for opaque types to TypeScript [1], and people have suggested various sorts of ways of approximating them, which revolve around the use of `unique symbol` declarations. However, I don’t fully understand these solutions and so thought it best not to include them in our public API. So, for now, let’s just use `unknown`, the same way as we do for `CipherParams.key`. Resolves #1442. [1] microsoft/TypeScript#202
I wasn’t sure of the best way to approach the typings of the modules (`Rest` etc). They should be opaque to the user (they shouldn’t be interacting with them directly and we want be free to change their interface in the future). There is an open issue to add support for opaque types to TypeScript [1], and people have suggested various sorts of ways of approximating them, which revolve around the use of `unique symbol` declarations. However, I don’t fully understand these solutions and so thought it best not to include them in our public API. So, for now, let’s just use `unknown`, the same way as we do for `CipherParams.key`. Resolves #1442. [1] microsoft/TypeScript#202
I wasn’t sure of the best way to approach the typings of the modules (`Rest` etc). They should be opaque to the user (they shouldn’t be interacting with them directly and we want be free to change their interface in the future). There is an open issue to add support for opaque types to TypeScript [1], and people have suggested various sorts of ways of approximating them, which revolve around the use of `unique symbol` declarations. However, I don’t fully understand these solutions and so thought it best not to include them in our public API. So, for now, let’s just use `unknown`, the same way as we do for `CipherParams.key`. Resolves #1442. [1] microsoft/TypeScript#202
This goes against the TypeScript principle that runtime equivalent types are equivalent. TypeScript promising to honor "nominals" would involve what worst case complexity in the CFA? Is it a type error to put them in flow superposition? Because once they are in superposition there is no way to separate them in runtime-space. The exists multiple ways to distinguish at runtime already, as has been shown above. Another is using Symbols, which JS provides specifically to be unique:
I am against this proposal because promising the ability to distinguish types without runtime backing could be both extremely complex and have with extreme limitations in practice. |
@craigphicks for anything that's an object, the runtime definitely backs it - it'd only be a unique capability for primitives (which in practice, would likely just be strings or numbers). |
@ljharb const s = Math.random() ? s1 : s2; If it is an error, then it is an error unlike any other type - currently this is never an error for any type.
What is the |
It's not useless just because it can't provide additional narrowing - it's just that some niche cases, like that one, won't benefit. However, this feature won't make it any worse. |
Why would that be an error? Your
TypeScript already has custom type guards and support for |
I am saying I think that should be an error, in order to warn users that the merging will be inseparable later. I am also saying that exactly highlights the limitations of nominal types. A nominal type can never be a discriminant. TypeScript's power lies in it's ability to track flow superpositions and allow discriminants to separate those superpositions, and also that other bookkeeping is outside of its remit. It's also think it is healthy that we disagree on this :) So I respect your opinion. |
@craigphicks it certainly can be a discriminant, often! it just can't always be one. |
It shouldn’t be an error because it is a valid thing to do. Especially if
I don’t know what TypeScript’s strengths, etc. are. To me, it allows me to get static type checking in a more explicit and community-supported way than Flow without significantly adding overhead (unless you’re targeting a version of JavaScript which doesn’t have built-in |
Since almost 10 years has passed from when the suggestion was first made, it doesn't look as if this is ever going to be supported. Anyone wanting to achieve this without vscode bringing up a prompt for the nominal property when typing, you can include a space at the start of the property name, like in the following example, because hacks are clearly the better choice :-s
|
I got sent here from #42534, about a type related issue that isn't caught by typescript (assigning a |
@nathan-chappell - In 5.4.5 the second line is an compile error (also a runtime error)
|
@craigphicks this is the issue: const arrayBuffer: ArrayBuffer = new Uint8Array()
const dataView = new DataView(arrayBuffer) |
@nathan-chappell I see. I guess the js run time error is caused by js using a test equivalent to However, in the TS library for JS objects, type representing instances of constructed objects do not have associations back to the constructors. For example Similarly for That's a design choice, and behind it is the presumption that the constructor doesn't matter - all that matters is the object structure. Perhaps it could be fixed by making the I think that's off topic for "non-structural (nominal) type matching" - which is the subject of this thread, because it is structural. |
@craigphicks Well I already agreed with the design choice. I haven't read all 600 posts here, but the point is that the structural subtyping is incorrect in this case due to unexposed details, and nominal subtyping would solve the issue. Here is a real case where this functionality is needed to avoid runtime errors that could ostensibly be found by typescript without overhauling the entire philosophy - that is, gradual nominal types, initially only where it is required for correctness. This isn't appropriate for this thread? |
Tying an interface to a specific class sounds to me like the definition of nominal typing. Branding, or any moral equivalent (e.g. |
Nominal typing for object types is currently available to users on a case-by-case using using symbols.
Using symbols also has the advantage that it corresponds to a runtime usable discriminant. (Note: Above I wrote "making the Typescript could allow a unique constructor representative thing similar in behavior to a unique symbol to be optionally made public and automatically used for assignability checking when it is present. That would be helpful in a number of cases - e.g.
That can't be done presently because anything |
I think I might have misunderstood part of your original comment. I thought you were suggesting that the distinction between |
@snarbies - I was actually interpreting "non-structural nominal typing" as applying only to TypeScript functionality of discriminating types for which there is no runtime way to distinguish between them (That may be an incorrect assumption about this thread and if so I apologize). That's certainly a topic in it's own right. In contrast this |
Thanks @nathan-chappell and @snarbies for your thought provoking feedback. I've submitted a proposal #58181 as an attempt to resolve the problem. |
@craigphicks Great, thanks, cheers. |
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:
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.
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.
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!):
The text was updated successfully, but these errors were encountered: