question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Use ES Symbol for type annotations

See original GitHub issue

Suggestion

TypeScript does not have any form of runtime type checking. I want to suggest an improvement to the compiler; for each interface and class, create a Symbol instance. This Symbol instance can safely be attached to any derived object without consequence.

I suggest this symbol is named a Fubber (phubber); a friend has started calling it this and it stuck.

Currently I manually declare the symbol. Since the symbol is a value and IHuman is an interface, I am allowed to reuse the same name:

export const IHuman = Symbol("IHuman");
export interface IHuman {
    [IHuman]: boolean; // The "Fubber"
}

When you then define a TypeScript class:

import { IHuman } from './IHuman';
class Human implements IHuman {
    [IHuman] = true; // I must manually set this
}

When I want to test if an object implements this interface, I simply do this:

import { IHuman } from './IHuman';
import { serviceProvider } from '../ServiceContainer';

let humanInstance = serviceProvider(IHuman) as IHuman;

By using the symbol as a key, type checking works for

Runtime type testing can be very useful in many scenarios where we’re developing libraries; for example if you want to find a module

🔍 Search Terms

TypeScript implements interface instanceof type checking type guards

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn’t be a breaking change in existing TypeScript/JavaScript code
  • This wouldn’t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript’s Design Goals.

I think it agrees with the design goals. This suggestion is simply taking advantage of a new feature in ES6; Symbols, and creates a parallel to certain functionality related to interfaces in other programming languages.

⭐ Suggestion

TypeScript should automatically declare ES6 symbols for all interfaces and classes. This symbol could be a const value with a name that matches the interface. The benefit of the const symbol is that it can be used from javascript as well.

Having a unique symbol for each class and interface allows software developers to implement stricter reasoning about the intention of an object; not merely only about the methods that the object implements.

When a class implements an interface, the compiler should add the symbol that belongs to that interface as a property key of the prototype with a value type of true. The type should however be true | false, so that an ancestral class can declare that it is too far removed from the original interface - effectively “un-implement” the parent class.

For consistency, I suggest that a symbol is also declared for classes.

Declaring interfaces:

const IDatabaseDriver = Symbol("IDatabaseDriver");
interface IDatabaseDriver {
    [IDatabaseDriver]: true | false;
}

Derived classes receive a new property:

class MySQL implements IDatabaseDriver {
    [IDatabaseDriver] = true; // this could be hidden
}

When importing IDatabaseDriver, you will import both the symbol and the interface. This is possible because the interface is a type (which is abstract).

The instanceof operator is augmented with an additional check if the symbol is available in the scope:

if (a instanceof B) { ... }

is compiled to

if (a instanceof B || a[B]) { ... }

An alternative approach is to introduce a new operator, but this would probably be less intuitive.

📃 Motivating Example

TypeScript does not have a way to quickly test if a class implements a certain contract. Type guards are slow, and can generate false positives if the object accidentally implements methods of the same name, and if you are like me; you would develop a library function that can generate the type guard for you dynamically. One suggestion is to “decorate” the interface with a property named “decorator” and a const string type, but this creates problems with extending interfaces. Fortunately; the ES6 Symbol can save the day!

💻 Use Cases

Contracts

The JavaScript/TypeScript ecosystem needs better interoperability between projects. You should be able to replace one database adapter with another database adapter, or a log implementation with another log implementation. Today, this seems to happen by coincidence (or by carefully replicating each others’ APIs.

This can easily be achieved by cooperatively defining contracts to solve common problems. Developers should be able to declaratively say that this library IS a database driver. In other languages, this is done via interfaces.

Service Injection

The biggest AHA moment when learning TypeScript was when I understood how everything related to types are completely handled by the compiler. This is a design choice I appreciate.

But I equally much dislike the type guards that people are encouraged to write; they can easily return false positives because having the same method names and properties does not imply that the implementation serves the same purpose.

A service injector should not by accident inject the console object, when you wanted a logger instance - simply because they have overlapping methods.

Modular Design

If you have a base Module class, you might want to differentiate between module capabilities. In other languages, this is declaratively done by implementing a particular interface.

In a language without classical OOP, Symbols serve the same purpose. A symbol is a unique value that can be used to “tag” objects, or they can be used as a key to access hidden information.

A Symbol on objects that implement an interface would be a parallel.

Workarounds

I’m currently using this approach to solve the problem. I just don’t like to type something that I feel can safely be implicit in the compiler. I have researched other approaches:

  • Type Guards; this is not strict typing - it is simply listening to see if the animal quacks, then concluding that it is a duck. It is harder to distinguish crocodiles from alligators; so the type guards can be quite elaborate.

  • “Discriminators”; slightly better than type guards, but does not work with inheritance.

  • Manually adding Symbols; best solution I’ve come up with - but since it creates a requirement for any future child class to have additional code - I feel the TypeScript compiler should implement this.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:2
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
MartinJohnscommented, Apr 28, 2021

Adding a type symbol to the prototype of a function adds no runtime overhead. It adds a few bytes to the files and for the parser, but after that it is merely a property that belongs to a prototype somewhere up the prototype chain.

Overhead is overhead. 😃 But yeah, it’s negligible. Tho you said it should be generated for all interfaces and classes, which is hefty.

In what way does Symbols affect the runtime behavior of JavaScript code?

By… the instance having an additional symbol assigned. And if you’re not using a module you suddenly have a weird symbol-variable in your file, which could collide with other code.

I don’t know your role with TypeScript, but if this is the official stance on feature suggestions I guess I’ll just shut up.

I don’t have a role with TypeScript. I’m just a developer that likes and uses TypeScript a lot, and I like to provide feedback. I’ve been fairly active here and I believe I have a good understanding of what language TypeScript aims to be. Adding any sort of runtime type information is stated again and again as out of scope.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Type Annotation in Python
Type annotations — also known as type signatures — are used to indicate the datatypes of variables and input/outputs of functions and methods....
Read more >
Understanding type annotation in Python
In this extensive post with specific examples, learn how to use Python type annotation to your advantage using the mypy library.
Read more >
Type Annotation in TypeScript
TypeScript - Type Annotations. TypeScript is a typed language, where we can specify the type of the variables, function parameters and object properties....
Read more >
Type inference and type annotations - Mypy documentation
We use an annotation to give it a more general type Union[int, str] (this type means that the value can be either an...
Read more >
Using the annotated-text field | Elasticsearch Plugins and ...
Any use of = signs in annotation values eg [Prince](person=Prince) will cause the document to be rejected with a parse failure. In future...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found