microsoft / microsoft/TypeScript

Proposal: Allow a name bound to a class value to be used in a type position to reference the class's instance type

Abierto
#36,348 9 comentarios 21 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

In Discussion Suggestion
Lenguaje dominante
Go
Estrellas
111k
Forks
14.3k
Merge medio
1 d 19 h
PR fusionados (30 d)
117

Descripción

Proposal: Allow a name bound to a class value to be used in a type position to reference the class's instance type

What?

In TypeScript, when a class is created and bound to a name via a class declaration, the name can be used in both value and type positions. In contrast, when a class is bound to a name via const, let, or var, the name can only be used in value positions.

I propose that when a class is bound to a name via const, let, or var, the name should be usable in both value and type positions, and that when such a name is used in a type position, it be interpreted as the instance type of the bound class value.

More formally:

  • A name should be deemed a ClassValueName if:
    • It is declared via a const, let, or var statement, and
    • Its type is assignable to new (...args: any[]) => any, and
    • Its type is not any
  • A ClassValueName should be usable in any type position that a class declaration name could be used in, and
  • A ClassValueName used in a type position should be interpreted as the instance type of the class value, in the same way that a class declaration name used in a type position is interpreted as the instance type of the class declaration
Examples of proposed behaviour
// `Foo` is of type `new () => {}`
const Foo = class {};

// `Foo` can be used in a type position here, and `foo` is of type `{}`
const foo: Foo = new Foo();
// `Foo` is of type `new <T>(value: T) => { value: T }`
const Foo = class <T> { constructor(public value: T) {} };

// `Foo` can be used in a type position here (and is generic), and `foo` is of type `{ value: number }`
const foo: Foo<number> = new Foo(42);
function newFooClass() {
  return class <T> { constructor(public value: T) {} };
}

// `Foo` is of type `new <T>(value: T) => { value: T }`
const Foo = newFooClass();

const foo: Foo<number> = new Foo(42);
const classes = {
  Foo: class <T> { constructor(public value: T) {} }
};

// `Foo` is of type `new <T>(value: T) => { value: T }`
const { Foo } = classes;

const foo: Foo<number> = new Foo(42);
const withBrand =
  <B extends string>(brand: B) =>
    <C extends new (...args: any[]) => {}>(ctor: C) =>
      class extends ctor {
        brand: B = brand;
      };

const Foo = class <T> { constructor(public value: T) {} };
// `FooWithBrand` is of type `new <T>(value: T) => ({ value: T } & { brand: 'Foo' })`
const FooWithBrand = withBrand('Foo')(Foo);

// `FooWithBrand` can be used in a type position here (and is generic), and `fooWithBrand` is of type `{ value: number } & { brand: 'Foo' }`
const fooWithBrand: FooWithBrand<number> = new FooWithBrand(42);

Why?

Unlike a class declaration, a class value requires a separate type declaration to expose the class's instance type

For class declarations, we can simply use the name of the class in a type position to reference its instance type:

class Foo {}

const foo: Foo = new Foo();

But for class values, a separate type declaration is required:

const Foo = class {};

type Foo = InstanceType<typeof Foo>;

const foo: Foo = new Foo();

Requiring a separate type declaration has a few issues:

  • It's inconsistent with class declarations (which don't require a manual type declaration)
  • It doesn't work for generic classes (see next section)
  • It's boilerplate (which adds up, especially with multiple classes in the same file)

With this proposal however, we wouldn't need a separate type declaration, and all of the following would just work:

const Foo = class {};

const foo: Foo = new Foo();
function newFooClass() { return class {}; }

const Foo = newFooClass();

const foo: Foo = new Foo();
const classes = { Foo: class {} };

const { Foo } = classes;

const foo: Foo = new Foo();
There is currently no way to access the generic instance type of a generic class value

None of the following work:

const Foo = class <T> { constructor(public value: T) {} };

const foo: Foo<number> = new Foo(42);
// => Error: 'Foo' refers to a value, but is being used as a type here.
const Foo = class <T> { constructor(public value: T) {} };

type Foo = InstanceType<typeof Foo>;

const foo: Foo<number> = new Foo(42);
// => Error: Type 'Foo' is not generic.
const Foo = class <T> { constructor(public value: T) {} };

type Foo<T> = InstanceType<typeof Foo<T>>;
// => Error: '>' expected.

const foo: Foo<number> = new Foo(42);
const Foo = class <T> { constructor(public value: T) {} };

type Foo<T> = InstanceType<typeof Foo><T>;
// => Error: ';' expected.

const foo: Foo<number> = new Foo(42);

With this proposal however, we could simply use the name of the class value in a type position to reference its generic instance type:

const Foo = class <T> { constructor(public value: T) {} };

const foo: Foo<number> = new Foo(42);
// => No error
It enables a potential workaround for #4881

This doesn't work:

const withBrand =
  <B extends string>(brand: B) =>
    <C extends new (...args: any[]) => {}>(ctor: C) =>
      class extends ctor {
        brand: B = brand;
      };

@withBrand('Foo')
class FooWithBrand<T> {
  constructor(readonly value: T) {}
}

type FooBrand<T> = FooWithBrand<T>['brand'];
// => Error: Property 'brand' does not exist on type 'FooWithBrand<T>'.

But with this proposal, we could do this:

const withBrand =
  <B extends string>(brand: B) =>
    <C extends new (...args: any[]) => {}>(ctor: C) =>
      class extends ctor {
        brand: B = brand;
      };

const FooWithBrand = withBrand('Foo')(
  class <T> {
    constructor(public value: T) {}
  }
);

type FooBrand<T> = FooWithBrand<T>['brand'];

While this wouldn't be type support for actual decorators, it would at least provide a means of class decoration that's reflected at the type level.

Flow supports this (there is prior art)

The following all work in Flow:

const Foo = class {};

const foo: Foo = new Foo();
const Foo = class <T> {
  value: T;
  constructor(value: T) { this.value = value; }
};

const foo: Foo<number> = new Foo(12);
function newFooClass() { return class {}; }

const Foo = newFooClass();

const foo: Foo = new Foo();
function newFooClass() {
  return class <T> {
    value: T;
    constructor(value: T) { this.value = value; }
  };
}

const Foo = newFooClass();

const foo: Foo<number> = new Foo(42);
const classes = { Foo: class {} };

const { Foo } = classes;

const foo: Foo = new Foo();
const classes = {
  Foo: class <T> {
    value: T;
    constructor(value: T) { this.value = value; }
  }
};

const { Foo } = classes;

const foo: Foo<number> = new Foo(42);

While feature parity with Flow is obviously not one of TypeScript's goals, Flow supporting this behaviour means that there's at least a precedent for it.

Why not?

It may be a breaking change

Depending on the implementation approach, this may be a breaking change.

For example, if this proposal were to be implemented by having the compiler automagically generate a separate type name whenever it encountered a ClassValueName, the generated type name may clash with an existing type name and break that currently working code.

On the other hand, if it were possible to implement this proposal using some kind of fallback mechanism (such as preferring any existing type name over using a ClassValueName in a type position, for instance), then the change would be backwards compatible.

It is currently unclear whether or not the proposed change can be implemented in a backwards compatible manner.

It "muddies" the separation between the value and type namespaces

TypeScript maintains separate namespaces for values and types. While this separation is fairly core to the language, there are already several exceptions in the form of class declarations, enums, and namespaces.

This proposal would introduce a new exception to that separation. This exception is particularly notable, as it would mark the first time a const, let, or var-declared name would be permitted in a type position. This is in contrast to all current exceptions, which each have construct-specific declaration syntax that differentiates them from standard variable declarations.

This proposal's "muddying" of the separation between the value and type namespaces may be confusing and/or surprising for both new and existing TypeScript users.

Next steps

  • Get feedback from both the TypeScript team and the community
  • Investigate potential implementation strategies

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code (unclear at this stage)
  • 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.

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

La propuesta no menciona archivos, pruebas ni puntos de entrada del compilador; empieza revisando las reglas de ClassValueName e investigando estrategias de implementación y la compatibilidad con el comportamiento existente del espacio de nombres de valores/tipos. Se considera completado cuando el comportamiento propuesto cuenta con comentarios del equipo y de la comunidad, además de una estrategia de implementación viable, tal como se indica en los próximos pasos del issue.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
typescript
Área
compilers
Tipo de issue
Nueva funcionalidad
Dificultad
5/5
Tiempo estimado
Más de una semana
Estado de actividad
Estancado
Claridad
Bastante claro
Aptitud para principiantes
25/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.