Search code examples
typescripttypescript-genericstypescript-class

How to construct a dual interface/value in Typescript without declaring Class


In Typescript, declaring a class magically creates both an interface and a value. But creating a class value through other means does not automatically make it a type, so e.g.

class Normal<T> {}
const Derived = Normal<number>

let normal: Normal<number> // <-- Fine and normal
let derived: Derived // <-- Derived is not a type

Somewhat amazingly, I discovered you can simply merge a type onto the value to manually mimic the class declaration, e.g.:

class Normal<T> {}
const Derived = Normal<number>
type Derived = Normal<number>

let normal: Normal<number> // <-- Fine and normal
let derived: Derived // <-- Now also fine

My question is this: If Derived is created dynamically, is there a way to get this dual interface/value without manually adding steps, e.g.

class Normal<T> {}

function derive<T>(generic: T) {
    const Derived = Normal<T>
    type Derived = Normal<T> // obviously will not work
    return Derived
}

const Derived = derive(number)

let normal: Normal<number>
let derived: Derived // <-- Derived is not a type

The context is I'm providing an API that creates many such classes and adds dynamic generics. I want to simplify the use of these classes for the API consumer.


Solution

  • The only way to get a class instance type to be declared without explicitly creating the type yourself is with a class declaration (relevant doc). There was a feature request a while ago at microsoft/TypeScript#18942 so that any named value whose type is a class constructor would automatically also cause an instance type of the same name to exist, but this was declined.

    Unless you're willing to write an actual class declaration such as

    class Derived extends Normal<number> { }
    

    then the only way to get a named type corresponding to your named value is to declare it separately as you discovered:

    const Derived = Normal<number>
    type Derived = Normal<number>
    

    (Note that this is not considered declaration merging; named types and named values live in different spaces that do not conflict. Declaration merging is when you refer to the same named value multiple times or the same named type multiple times.)

    Playground link to code