Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Force generic type to be the type of a unique symbol in typescript

After reading this answer about nominal typing I thought to move it one step further with

export type MARKED<C, M> = C & {
  readonly m: M;
};

and to be used like

const readonly SPECIAL: unique symbol = Symbol();
isSpecial(value: C): value is MARKED<C, typeof SPECIAL> {
  return someVerificationOf(value);
}

Here I try to be extra safe by replacing the "brand" with a unique symbol, though MARKED could as well be used with just a string as

class Foo { ... };
type SpecialFoo = MARKED<Foo, 'special'>;

Is there a chance to force the 2nd generic parameter of MARKED to be a unique symbol? I tried things like

type MARKED<C, M extends unique symbol> = ...

but this is obviously wrong.

like image 707
Harald Avatar asked Nov 15 '25 18:11

Harald


1 Answers

Please see the docs

To enable treating symbols as unique literals a new type unique symbol is available. unique symbol is a subtype of symbol, and are produced only from calling Symbol() or Symbol.for(), or from explicit type annotations. The new type is only allowed on const declarations and readonly static properties, and in order to reference a specific unique symbol, you’ll have to use the typeof operator. Each reference to a unique symbol implies a completely unique identity that’s tied to a given declaration.

Especially this: in order to reference a specific unique symbol, you’ll have to use the typeof operator

Hence, in order to use unique symbol constraint, you need to use typeof:


const UNIQUE: unique symbol = Symbol();

type MARKED<U extends typeof UNIQUE> = U

type Result = MARKED<typeof UNIQUE>

But, I'd willing to bet, that you have several unique symbols and you don't want to create utility type for each symbol.

Let's take a look closer to this line: unique symbol is a subtype of symbol.

What it gives us?

unique symbol is a subtype of symbol. It gives us a door for checking whether some type is a subtype of symbol or not. It is already something.

So, we need to check whether passed type is a subtype of symbol:

type IsUnique<UniqueSymbol extends symbol> =
    (UniqueSymbol extends symbol
        ? (symbol extends UniqueSymbol
            ? false
            : true
        )
        : false
    )

// true
type Check = IsUnique<symbol & { tag: 42 }>

Above util does not meet our requirements.

We need also to check if symbol contains only symbol keys.

type IsExactKeys<S extends symbol> =
    (keyof S extends SYMBOL_KEYS
        ? (SYMBOL_KEYS extends keyof S
            ? true
            : false
        )
        : false
    )

type IsUnique<UniqueSymbol extends symbol> =
    (UniqueSymbol extends symbol
        ? (symbol extends UniqueSymbol
            ? false
            : IsExactKeys<UniqueSymbol>
        )
        : false
    )
// never
type Check = IsUnique<never>

Ooops, it still does not work with never.

Let's handle never type:


const UNIQUE: unique symbol = Symbol();
const REGULAR: symbol = Symbol()

type SYMBOL_KEYS = keyof symbol;

type IsExactKeys<S extends symbol> =
    (keyof S extends SYMBOL_KEYS
        ? (SYMBOL_KEYS extends keyof S
            ? true
            : false
        )
        : false
    )

// credits goes to https://github.com/microsoft/TypeScript/issues/40248
type IsNever<T> = [T] extends [never] ? true : false;

type IsUnique<UniqueSymbol extends symbol> =
    (IsNever<UniqueSymbol> extends true ? false
        : (UniqueSymbol extends symbol
            ? (symbol extends UniqueSymbol
                ? false
                : IsExactKeys<UniqueSymbol>
            )
            : false
        )
    );

type Assert<T extends true> = T

type Unique = {
    readonly StaticSymbol: unique symbol
}['StaticSymbol']

/**
 * Ok
 */
type Test0_0 = Assert<IsUnique<Unique>> // ok
type Test0 = Assert<IsUnique<typeof UNIQUE>> // ok

/**
 * Expected errors
 */
type Test1 = Assert<IsUnique<typeof REGULAR>> // error
type Test2 = Assert<IsUnique<
    & typeof REGULAR
    & { readonly tag: unique symbol }
>> // error

type Test3 = Assert<IsUnique<symbol>> // error
type Test4 = Assert<IsUnique<any>> // error
type Test5 = Assert<IsUnique<unknown>> // error
type Test6 = Assert<IsUnique<never>> // error


export type MARKED<C, M extends symbol> =
    IsUnique<M> extends true ? C & {
        readonly m: M;
    } : never

Playground

Seems to be it works now. Please keep in mind, above constraint is indirect. I'd say that it is a duck constraint.

If something is:

  • a subtype of symbol
  • has same keys as a symbol
  • is not never
  • symbol is not assignable to passed type but passed type is assignable to symbol

then, probably we have a deal with unique symbol.

Please add more tests.

Please trust above code only for 90%.

like image 114
captain-yossarian Avatar answered Nov 17 '25 07:11

captain-yossarian



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!