microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.34k stars 12.4k forks source link

Improve handling of "unexported symbols" #48987

Open entropitor opened 2 years ago

entropitor commented 2 years ago

Suggestion

When declaration is set to true (e.g. through composite projects), every inferred type needs to be accessible:

File 1: a creator function of an "abstract type" (think https://github.com/microsoft/TypeScript/issues/202)

const MySymbol = Symbol('MySymbol');
type MyThing = {
    _brand: typeof MySymbol,
    onlyCreatedHere: 'more-or-less'
}
const createMyThing = () => {
  return {
    _brand: MySymbol,
    onlyCreatedHere: 'more-or-less'
  }
}

File 2: builds on top of this

const createEverything = () => {
  return {
    myThing: createMyThing()
  }
}

We get a type error:

The inferred type of 'createEverything' references an inaccessible 'unique symbol' type. A type annotation is necessary.

It can be solved by exporting MySymbol but that's against the point of the symbol as we want to make it private to hide things

It would be nice if TypeScript could automatically export MySymbol in a private matter. Other declaration files can import the type/value but actual files not.

Another solution would be that if the type of the Symbol is exported (export type MySymbol = typeof MySymbol) that that type is used and declared as an unique symbol (declare export type MySymbol = unique symbol).

🔍 Search Terms

symbol declaration inaccessible

✅ Viability Checklist

RyanCavanaugh commented 2 years ago

This seems very unlikely to be something we do -- it implies that either file2 is allowed to take a dependency on unexported members of file1, which means that editing file1.d.ts (not something you would do but we can't distinguish generated and nongenerated files) would be full of hidden refactoring hazards, or that the generation of file1.d.ts depends on what kind of things use it, which is very difficult to reason about as a user.

There are definitely things you can write today to remove the error.

entropitor commented 2 years ago

What are other ways to remove the error? Is there a way to not export the symbol?

Would it be an option to improve the export when there is a type export:

const MySymbol = Symbol('MySymbol');
export type MySymbol = typeof MySymbol

currently generates

declare const MySymbol: unique symbol;
export declare type MySymbol = typeof MySymbol;
export {};

which means that even if we export a type, we still get the same error. If the type export could somehow be referencing the symbol directly, the problem would be solved

aapoalas commented 1 year ago

I've run into this same issue. I have a hierarchy of classes that are exposed to users that hold hidden and potentially dangerous values inside private members. I would very much like to encapsulate these hidden values in such a way that it is actually impossible for user code to access them, while the classes can in some special cases use a shared symbol to access each other's private members through this shared symbol. They receive the shared symbol as a constructor parameter, passed through from the very top of the hierarchy. The symbol is never exported from any file and is always kept in private members by classes (if they need to) to make sure it is not accessible to users in any way or form.

So something like this:

// root.ts
import { Child } from "./child";
const SPECIAL_SYMBOL = Symbol("[[special]]");
export type SpecialSymbol = typeof SPECIAL_SYMBOL;

// Public API
export class Root {
  createChild() {
    return new Child(SPECIAL_SYMBOL);
  }
}

// child.ts
import { type SpecialSymbol } from "./root";

export class Child {
  #specialSymbol: SpecialSymbol;
  [key: SpecialSymbol]: SomePrivateValueType;

  constructor(specialSymbol: SpecialSymbol) {
    this.#specialSymbol = specialSymbol;

    this[specialSymbol] = somePrivateValue; // Possibly a function to bind to `this`.
  }
}

Unfortunately, this does not work and instead I get this error:

An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.

Which is... kind of fair. I don't want to declare my class to be Record-like or anything like that. But, trying to do a computed property name leads to two errors:

A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type. 'SpecialSymbol' only refers to a type, but is being used as a value here.

The first points to the typeof SPECIAL_SYMBOL having lost its uniqueness. The second is kind of fair as I am indeed trying to sort of use a type as a value, though the error persist even if I declare [SpecialSymbol]: SomePrivateValueType.

I have not been able to find any way to do this except adding a lot of @ts-expect-error comments here and there.

aapoalas commented 1 year ago

One solution for this is this to use a mapped type interface extension:

type U = {
  [key in SpecialSymbol]: SomePrivateValueType;
}

interface Child extends U {}
class Child {
  constructor(specialSymbol: SpecialSymbol) {
    this[specialSymbol] = somePrivateValue; // Now this doesn't error.
  }
}