Repository: TypeScript
Issue: Type discrimination by static readonly symbol
Labels: Suggestion,Awaiting More Feedback

### 🔍 Search Terms

"type discrimination", "discrimination", "symbol discriminator"

### ✅ Viability Checklist

- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x] This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- [x] This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

### ⭐ Suggestion

AFAIK it is impossible to discriminate types by a static symbol like this:
```ts
class Animal {
    public static readonly symbol = Symbol()
}

if (obj[Animal.symbol] === something) {
    // here types are not deduced correctly
}
```
In contrast, code like this works fine:
```ts
const animal_symbol = Symbol()

if (obj[animal_symbol] === something) {
    // here types are not deduced correctly
}
```
Not sure if this is a bug or just not implemented due to some complications.

### 📃 Motivating Example

By using `unique symbol` in a class I can discriminate objects like this:

```ts
class Cat {
    public static readonly symbol = Symbol(`Cat.symbol`)

    public get symbol() : typeof Cat.symbol {
        return Cat.symbol
    }

    public meow() {}
}

class Dog {
    public static readonly symbol = Symbol(`Dog.symbol`)

    public get symbol() : typeof Dog.symbol {
        return Dog.symbol
    }

    public woof() {}
}

type AnimalUnion = Cat | Dog

function take_animal(animal : AnimalUnion) {
    if (animal.symbol === Cat.symbol) animal.meow()
    else if (animal.symbol === Dog.symbol) animal.woof()
    else assert_never(animal, new Error)
}

function assert_never(never : never, error : Error) : never {
    throw error
}
```
The main focus here is inside `take_animal`: we can know that `animal` has `meow()` method if `animal.symbol === Cat.symbol`.

In this particular case I used `symbol` key to store the symbol. But what if key is also a symbol?
Like this:

```ts
const animal_symbol = Symbol(`Animal.symbol`)

class Cat {
    public static readonly [animal_symbol] = Symbol(`Cat.symbol`)

    public get [animal_symbol]() : typeof Cat[typeof animal_symbol] {
        return Cat[animal_symbol]
    }

    public meow() {}
}

class Dog {
    public static readonly [animal_symbol] = Symbol(`Dog.symbol`)

    public get [animal_symbol]() : typeof Dog[typeof animal_symbol] {
        return Dog[animal_symbol]
    }

    public woof() {}
}

type AnimalUnion = Cat | Dog

function take_animal(animal : AnimalUnion) {
    if (animal[animal_symbol] === Cat[animal_symbol]) animal.meow()
    else if (animal[animal_symbol] === Dog[animal_symbol]) animal.woof()
    else assert_never(animal, new Error)
}

function assert_never(never : never, error : Error) : never {
    throw error
}
```

This example works fine and guarantee no name collisions between string key `symbol` and possible field in derived class.


But for some reason if I try to put `animal_symbol` in a `static readonly` property;

```ts
class Animal {
    public static readonly symbol = Symbol(`Animal.symbol`)
}

class Cat {
    public static readonly [Animal.symbol] = Symbol(`Cat.symbol`)

    public get [Animal.symbol]() : typeof Cat[typeof Animal.symbol] {
        return Cat[Animal.symbol]
    }

    public meow() {}
}

class Dog {
    public static readonly [Animal.symbol] = Symbol(`Dog.symbol`)

    public get [Animal.symbol]() : typeof Dog[typeof Animal.symbol] {
        return Dog[Animal.symbol]
    }

    public woof() {}
}

type AnimalUnion = Cat | Dog

function take_animal(animal : AnimalUnion) {
    if (animal[Animal.symbol] === Cat[Animal.symbol]) animal.meow()
    else if (animal[Animal.symbol] === Dog[Animal.symbol]) animal.woof()
    else assert_never(animal, new Error)
}

function assert_never(never : never, error : Error) : never {
    throw error
}
```

The trick doesn't work anymore. Now `take_animal()` can't deduce `meow()` from `animal[Animal.symbol] === Cat[Animal.symbol]`.
But I think it should.

### 💻 Use Cases

1. What do you want to use this for?
I want to use this to introduce segregation not by string key, but by a symbol key stored as `static readonly`.
3. What shortcomings exist with current approaches?
It cannot use `static readonly` symbols.
5. What workarounds are you using in the meantime?
I have to use symbols as global variables.