Skip to content
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

Allow functions to have new symbol as the return type #37469

Open
5 tasks done
ExE-Boss opened this issue Mar 19, 2020 · 5 comments
Open
5 tasks done

Allow functions to have new symbol as the return type #37469

ExE-Boss opened this issue Mar 19, 2020 · 5 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Mar 19, 2020

Search Terms

  • unique symbol
  • new unique symbol
  • new symbol

Suggestion

I request that unique symbol be allowed as the return type of a function declaration.

Alternatively, it might be better to use new symbol to disambiguate #40106 (comment).

Use Cases

Currently, it’s impossible to create an alias or a wrapper function for the global Symbol constructor and use that to construct unique symbols:

// ./es-globals/fundamentals.js
export const ESSymbol = Symbol;
export const { for: ESSymbol_for } = ESSymbol;
// ./es-globals/fundamentals.d.ts
export import ESSymbol = globalThis.Symbol;
export declare const ESSymbol_for: typeof ESSymbol.for;
// ./symbols.js
import { ESSymbol, ESSymbol_for } from "./es-globals/fundamentals.js";

// should be `unique symbol`, but is instead `symbol`:
export const customSymbol = ESSymbol("custom");

// should be `unique symbol` or `global symbol "nodejs.util.inspect.custom"`,
// but is instead `symbol`:
export const nodejs_util_inspect_custom = ESSymbol_for("nodejs.util.inspect.custom");

// should be `unique symbol` or `global symbol "nodejs.util.promisify.custom"`,
// but is instead `symbol`:
export const nodejs_util_promisify_custom = ESSymbol_for("nodejs.util.promisify.custom");

Examples

This would allow defining SymbolConstructor as:

declare interface SymbolConstructor {
	/**
	 * Returns a new unique Symbol value.
	 * @param description Description of the new symbol value.
	 */
	(description?: string | number): new symbol;

	/**
	 * Returns a Symbol object from the global symbol registry matching the given key if found.
	 * Otherwise, returns a new symbol with this key.
	 * @param key key to search for.
	 */
	for<T extends string>(key: T): global symbol T;
	// or, until GH-35909 is implemented:
	for(key: string): new symbol;
}

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

See also

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Mar 19, 2020
@Shou
Copy link

Shou commented May 11, 2020

You can sort of get around it using some epik type-level hax, but I don't know if as casting is cheating.

const sym: (a: string) => { readonly 0: unique symbol }[0]
  = a => Symbol(a) as ReturnType<typeof sym>

const bad = sym("wat")
// const bad: symbol
const good: ReturnType<typeof sym> = sym("neat")
// const good: unique symbol

@ExE-Boss ExE-Boss changed the title Allow functions to have unique symbol as the return type Allow functions to have unique symbol or new symbol as the return type Jan 25, 2021
@DerekZiemba
Copy link

DerekZiemba commented Mar 3, 2021

I tried everything even @Shou's hack. There is no work around and we def need some changes.

My biggest issue is that when you destructure an array of unique symbols, typescript no longer considers them unique.. And there is no way to force typescript to consider them unique.

// What I originally expected to work
const [$arr, $head, $tail] = 'array head tail'.split(' ').map(Symbol.for);
// A computed property name in a class property declaration must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1166)
interface TestClass { [$arr]: boolean; } // using interface for this test because then I don't have to come up with new name for each example

// Can't cast to unique symbol. Err: 'symbol' expected.ts(1005)
const [$arrAofUS, $headAofUS, $tailAofUS] = 'array head tail'.split(' ').map(Symbol.for) as unique symbol[];
const [$arrCAofUS, $headCAofUS, $tailCAofUS] = 'array head tail'.split(' ').map(Symbol.for) as Array<unique symbol>;
// A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1169)
interface TestClass { [$arrAofUS]: boolean; [$arrCAofUS]: boolean; }

// None of these work either
const [$arrEAofS, $headEAofS, $tailEAofS] = 'array head tail'.split(' ').map(Symbol.for) as [symbol, symbol, symbol];
// 'unique symbol' types are not allowed here.ts(1335)
const [$arrEAofUS, $headEAofUS, $tailEAofUS] = 'array head tail'.split(' ').map(Symbol.for) as [unique symbol, unique symbol, unique symbol];
// A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1169)
interface TestClass { [$arrEAofS]: boolean; [$arrEAofUS]: boolean; }

// It's not the split messing it up.  Error: 'unique symbol' types are not allowed here.ts(1335)
const [$arrAM, $headAM, $tailAM] = ['array', 'head', 'tail'].map(Symbol.for);
// Doesn't work with destructuring!
const [$arrD, $headD, $tailD] = ([Symbol.for('array'), Symbol.for('head'), Symbol.for('tail')]);
// A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1169)
interface TestClass { [$arrAM]: boolean; [$arrD]: boolean; }

// Even when trying @Shou's hack
type UniqueSymbol = ReturnType<(a: string) => { readonly 0: unique symbol }[0]>;

// testing if UniqueSymbol works on the simple cases
// NOPE! Err: Type 'typeof $test' is not assignable to type 'typeof 0'.ts(2322)
const $testCustomUniqueSymbol: UniqueSymbol = Symbol.for('test');
// This does if it's casted before assignment
const $testCastedCustomUniqSym: UniqueSymbol = Symbol.for('test') as UniqueSymbol;
// but it's still not a unique symbol! Err: Type 'typeof 0' is not assignable to type 'typeof $testUniqSym'.ts(2322)
const $testNativeUniqSym: unique symbol = $testCustomUniqueSymbol;
// however it does work for regular symbol
const $testNativeSym: symbol = $testNativeUniqSym;

// Attempt at destructuring again, and even explicit casting
const [$arrFnUS, $headFnUS, $tailEFnUS]: [UniqueSymbol, UniqueSymbol, UniqueSymbol] = 'array head tail'.split(' ').map(Symbol.for) as [UniqueSymbol, UniqueSymbol, UniqueSymbol];
interface TestClass {
  [$testCustomUniqueSymbol]: boolean;   // Works so we know our UniqueSymbol type works for some cases
  [$testCastedCustomUniqSym]: boolean;  // Works
  [$testNativeUniqSym]: boolean;        // Works
  // Surprised testNativeSym doesn't work!
  [$testNativeSym]: boolean;  // Err: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1169)
  [$arrFnUS]: boolean;        // Err: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1169)
}


// Attempts to hack around it:
// Err: Type 'symbol[]' is not assignable to type '(unique symbol)[]'. Type 'symbol' is not assignable to type 'unique symbol'.ts(2322)
const toSymArr = (str: string): UniqueSymbol[] => str.split(' ').map(Symbol.for);
// hack around the error
const toSymArr2 = (str: string): UniqueSymbol[] => str.split(' ').map(Symbol.for) as any;
// will it work?
const [$arrFn, $headFn, $tailFn] = toSymArr('array head tail');
// nope! Err: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1169)
interface TestClass { [$arrFn]: boolean;  }

image

@parzhitsky
Copy link

parzhitsky commented Jun 13, 2022

I need this feature do define a method of a collection class that creates unique tokens for each of its items:

interface Entry<Value> {
    readonly id: unique symbol;
    value: Value;
}

class Collection<Item> {
    private entriesMap = new Map<symbol, Entry<Item>>();

    private createEntryID(): symbol {
        const id: unique symbol = Symbol('item');

        return id;
    }

    add(item: Item): void {
        const id = this.createEntryID();

        // `symbol`, cannot be casted to `unique symbol`
        const entry: Entry<Item> = {
            value: item,
            id,
//          ^ Error: Type 'symbol' is not assignable to type 'unique symbol'.
        };

        this.entriesMap.set(id, entry);
    }
}

Try it.

So far, I can only fix this by ditching the unique requirement:

interface Entry<Value> {
  readonly id: symbol;
  value: Value;
}

Try it.

… and, since I don't much understand unique symbol, this is what I'm going to go with.


Starting a new season of the "How much "more feedback" do you need, @RyanCavanaugh" series 😉

@RyanCavanaugh
Copy link
Member

Relative to the cost here, quite a bit more 🙃. 15 upvotes and 3 comments in two years is not a lot.

@ExE-Boss ExE-Boss changed the title Allow functions to have unique symbol or new symbol as the return type Allow functions to have ~unique symbol~ or new symbol as the return type Mar 16, 2023
@ExE-Boss ExE-Boss changed the title Allow functions to have ~unique symbol~ or new symbol as the return type Allow functions to have new symbol as the return type Mar 16, 2023
@eligrey
Copy link

eligrey commented Jun 12, 2023

This bug affects a system in Transcend Consent for securely exposing subsets of class APIs to untrusted third party scripts. We have a createInterfaceViewerKeypair function that returns two unique symbol/new symbol that cannot currently be typed correctly in TypeScript. We work around this by casting to unknown but this is not ideal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants