microsoft / microsoft/TypeScript

Type discrimination by static readonly symbol

Open
#60,318 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Awaiting More Feedback Suggestion
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

🔍 Search Terms

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

✅ Viability Checklist
⭐ Suggestion

AFAIK it is impossible to discriminate types by a static symbol like this:

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:

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:

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:

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;

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.
  2. What shortcomings exist with current approaches?
    It cannot use static readonly symbols.
  3. What workarounds are you using in the meantime?
    I have to use symbols as global variables.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The issue names no files or tests; start with the take_animal Cat/Dog examples and reproduce the difference between a module-level symbol and Animal.symbol. Trace the type-checker behavior for static readonly symbol properties, then add focused coverage showing the intended narrowing and exhaustive assert_never behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.