Repository navigation
Support some non-structural (nominal) type matching #202
Description
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!):
// Define FooTable and FooIndex
nominal FooIndex = string; // Proposed new kind of nominal declaration.
interface FooTable {
[i: FooIndex]: { foo: number };
}
let s1: FooIndex;
let t1: FooTable;
// Define BarTable and BarIndex
nominal BarIndex = string; // Proposed new kind of nominal declaration.
interface BarTable {
[i: BarIndex]: { bar: string };
}
let s2: BarIndex;
let t2: BarTable;
// For assignment from base-types and basic structures: no type-overloading is needed.
s1 = 'foo1';
t1 = {};
t1[s1] = { foo: 1 };
s2 = 'bar1';
t2 = { 'bar1': { bar: 'barbar' }};
console.log(s2 = s1); // Proposed to be type error.
console.log(s2 == s1); // Proposed to be type error.
console.log(s2 === s1); // Proposed to be type error.
t1[s2].foo = 100; // Gives a runtime error. Proposed to be type error.
t1[s1].foo = 100;
function BadFooTest(t: FooTable) {
if (s2 in t) { // Proposed to be type error.
console.log('cool');
console.log(t[s2].foo); // Proposed to be type error.
}
}
function GoodBarTest(t: BarTable) {
if (s2 in t) {
console.log('cool');
console.log(t[s2].bar);
}
}
BadFooTest(t1); // Gives runtime error;
BadFooTest(t2); // No runtime error, Proposed to be type error.
GoodBarTest(t1); // Gives runtime error; Proposed to be type error.
GoodBarTest(t2);
Activity
RyanCavanaugh commentedon Jul 23, 2014
Is there a better keyword here than "abstract" ? People are going to confuse it with "abstract class".
+Needs Proposal
iislucas commentedon 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 commentedon Jul 23, 2014
Regarding a name, what about "nominal" types? Seems pretty common in literature.
RyanCavanaugh commentedon 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 commentedon Jul 23, 2014
Ryan Cavanaugh (@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 commentedon Jul 23, 2014
iislucas (@iislucas) no implementation is necessary for "Needs Proposal" issues, just something on the more formal side like Ryan described. No rush ;)
[-]Support non-structural (abstract) types[/-][+]Support some non-structural (nominal) type matching[/+]zpdDG4gta8XKpMCd commentedon Jul 25, 2014
There is a workaround that I use a lot in my code to get nominal typing, consider:
iislucas commentedon Jul 26, 2014
Neat trick! Slight optimization, you can use:
(Slightly nicer/safer looking syntax)
[Shame it doesn't work for creating distinct types for string that you want to be indexable]
basarat commentedon 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
stringon 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):Steve-Fenton commentedon 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.
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 commentedon Jul 31, 2014
👍
Steve-Fenton commentedon 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:
503 remaining items
nathan-chappell commentedon Apr 11, 2024
I got sent here from #42534, about a type related issue that isn't caught by typescript (assigning a
Uint8Arrayto anArrayBuffer). Quite frankly, the TS guys had me at we do structural subtyping and don't support nominal subtyping, I mean there you go. BUT, if the typing system leads to a small number of incompatibilities with the existing language runtime, then could the TS compiler at least recognize these? I guess it's complicated by the web/node/etc differences, but I would be happy if TS could recognize these known issues that are baked-in and give me a heads up...craigphicks commentedon Apr 11, 2024
Nathan Chappell (@nathan-chappell) - In 5.4.5 the second line is an compile error (also a runtime error)
nathan-chappell commentedon Apr 11, 2024
Craig P Hicks (@craigphicks) this is the issue:
craigphicks commentedon Apr 12, 2024
Nathan Chappell (@nathan-chappell) I see.
I guess the js run time error is caused by js using a test equivalent to
instanceof ArrayBuffer, that uses the constructor associated with passed object inside the constructor forDataViewHowever, in the TS library for JS objects, type representing instances of constructed objects do not have associations back to the constructors.
For example
ArrayBufferis declared a variable of typeArrayBufferConstructor, but the type returned bynew ArrayBuffer()doesn't have a reference to the constructor. It's not in the type on<object>.prototype.constructor.Similarly for
Uint8Array.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
prototype.constructorpublic on the interface of objects for which assignability should require the same constructor.I think that's off topic for "non-structural (nominal) type matching" - which is the subject of this thread, because it is structural.
nathan-chappell commentedon Apr 12, 2024
Craig P Hicks (@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?
snarbies commentedon Apr 12, 2024
Tying an interface to a specific class sounds to me like the definition of nominal typing. Branding, or any moral equivalent (e.g.
prototype.constructor), is an attempt to emulate nominal typing in a structural system. And in general theprototype.constructorsolution would fail if two types had structurally equivalent constructors. The situation here is genuinely two nominally distinct types.craigphicks commentedon Apr 12, 2024
Nathan Chappell (@nathan-chappell) snarbies
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
prototype.constructorpublic" but that was wrong because obviously that would only share the type of the constructor).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.
instanceofdoes actually expose that uniqueness.Array.filter,That can't be done presently because anything
Boolean-like would pass the assignment test but it could be a different class with different behavior.snarbies commentedon Apr 12, 2024
I think I might have misunderstood part of your original comment. I thought you were suggesting that the distinction between
ArrayBufferandUInt8Arraywas structural and thus off-topic, but perhaps you meant yourprototype.constructoridea was off-topic. If that's the case, sorry for misunderstanding.craigphicks commentedon Apr 12, 2024
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
ArrayBuffervs.UInt8Arrayproblem is about a case where runtime can discriminate, but TypeScript cannot. Even if overlaps with this thread, it has a component worth of being in another thread.craigphicks commentedon Apr 13, 2024
Thanks Nathan Chappell (@nathan-chappell) and snarbies for your thought provoking feedback. I've submitted a proposal #58181 as an attempt to resolve the problem.
nathan-chappell commentedon Apr 13, 2024
Craig P Hicks (@craigphicks) Great, thanks, cheers.
emilioplatzer commentedon May 10, 2025
Workarround
I use a lot this workarround. Here is an example of 800+ commits and 5 years of use
The folowing example is in Playground
Typing by example
Let's say that I'm writing a Survey App. I have surveys, questions and options (possible answers to the questions). I store all of it in the DB, each of them has a string key (but can be an integer one).
Then I declare the types
I can declare indexes and functions and all works (and checks) as expected.
The only caveat is declaring some values in the code, then you must cast it (but I think that in nominal typing you also must):
See an example (with lots of assumptions about the db):
Details
result.answersPerQuestion[question.id].push(question.id)you got Argument of type 'QuestionId' is not assignable to parameter of type 'OptionId'. Type '"Q1"' is not assignable to type 'OptionId'.
add `BrandedLink` (#620)
bluepnume commentedon May 26, 2026
Real-world example of where opaque types would provide concrete value.
In production we simulate opaque types via intersection types across many numeric primitives:
Milliseconds,Seconds,Minutes,Hours,Days,Cents,Percentage,Bytes,Pixels, and more. It works, but maintaining it requires custom tooling: aTagged<>helper, an ESLint rule to confine casts to designated boundaries, and a library of math wrappers (add,subtract, etc.) to preserve opaque identity across arithmetic.If opaque types shipped, the type identity becomes a first-class feature:
And operator overloading could layer naturally on top, so opaque identity survives arithmetic:
Critically, this is strictly additive: existing opaque-typed arithmetic that relies on the implicit upcast to
numberkeeps working unchanged. Opaque identity only survives when a matching overload is in scope.#42218 is an alternative path to the same result, bundling opaque types and operator overloading into a single
primitivedeclaration. Detailed writeup of how that would look in our codebase: #42218 (comment).Either path turns a widely-used workaround into a first-class language feature.
Strong support.
irfanstract commentedon Aug 27, 2026
Allow extends clause in interfaces to name union types in place of known-name object types. This avoids the self-dealiasing nature of type-aliases (eg
type T = ...,type T = T1 | T2 | ...), instead acting as nominal types, preserved across assignments, calls and inference (interfaces are generally inferred as-is, not dealiased).the compiler should then see:
just as if it had been
type CommonAction = GlobalRefreshAction | FetchAction | UpdateAction, except that it's now a nominal type (interface) instead.if wanted, we can introduce a 'special marker type'
Opaque, permitted inextendsclause ofinterfaces (C extends Opaque<T>) to mark distinct opaque type (Opaque<T>upper-bounded byT). usage ofOpaqueelsewhere to be rejected.