Skip to content

Allow object types to have property-like associated types #17588

Open
@dead-claudia

Description

@dead-claudia

Edit: Add static Type variant for classes, make abstractness a little clearer, clarify things.

This has probably already been asked before, but a quick search didn't turn up anything.

This is similar, and partially inspired by, Swift's/Rust's associated types and Scala's abstract types.


Rationale

  • In complex structures like in virtual DOM components/nodes and in graphs, where there are numerous constraints to check, but they would get cumbersome in a hurry to force off onto the user.
  • In dynamic imports, where you never have the module namespace itself until runtime, yet you still want to be able to use types defined within it. Edit: Already addressed elsewhere.
  • In types where there's a lot of optional parameters, it'd be easier and more flexible to label which ones you're defining as part of the type.
  • In types where you want to specify a single optional parameter, and the required ordering requires you to specify some that you don't want to specify, that would get boilerplatey in a hurry. I've experienced this personally on multiple occasions.
  • It would allow fully typing namespaces and named module imports without hard-coded compiler handling.

Proposed Syntax/Semantics

  • Each interface and object type may have a number of associated types within it.
  • Associated types are checked for assignability like any other usual property, except they live in a different "namespace" from properties. In particular, the type must be assignable to the target's type.
  • To access an associated type, you use TypeName.Type, where Type is the name of an associated type.
    • For sugar and namespace compatibility, object.Type is equivalent to (typeof object).Type
  • To expect an interface with an associated type, you use Foo & {type Type: Value} or Foo with <Type: Value>.
  • To expect an object with an associated type, you use {type Type: Value}.
  • To declare an abstract associated type, you use type Type: * within the interface or object type.
  • To declare a non-abstract associated type, you use type Type: Default within the interface or object type.
  • To constrain an abstract associated type, you use type Type: * extends Super.
  • Associated types may be inherited from other interfaces and/or object types.
  • Namespaces' types are modified to include the exported types as associated types.
  • Classes may also define associated types, optionally with visibility modifiers.
  • Associated types may have defaults, in case they aren't defined or further constrained later.
    • Constraining an associated type with an existing default removes the default if and only if the existing constraint is not assignable to the new constraint. For example, {type Foo: string | number = string} & {type Foo: string | number} is assignable to {type Foo: string}, but {type Foo: string | number = string} & {type Foo: number} is not.

Here's what that would look like in syntax:

// Interfaces
interface Foo {
    type Type: *; // abstract
    type Sub: * extends Type; // abstract, constrained
    type Type: Default;
    type Type: * extends Type = Default; // late-bound default
}

// Objects
type Foo = {
    type Type: Foo,
}

// Classes
abstract class Foo {
    // Note: outer class *must* be abstract for these, and the keyword is required.
    abstract type Type: *; // abstract
    private abstract type Sub: * extends Type; // abstract, constrained
    protected abstract type Type: * extends Type = Default, // late-bound default

    // Note: outer class *may* be not abstract for these.
    type Type: Default;
    private type Type: Default;

    // Declare an associated type in the class
    // Note: type must not be abstract.
    static Type: Foo;
}

Emit

This has no effect on the JavaScript emit as it is purely type-level.

Compatibility

  • This is purely additive, making no observably incompatible changes beyond possibly different error messages.
  • This has no impact on any existing JavaScript-related proposal.

Other

  • It may slow down the type checker a little initially when namespaces are unified, but two optimization points are available, which would recover most of the perf hit, if not all:
    • Associated types could be stored in a per-interface type map.
    • The list of associated types could be initially stored as undefined to avoid generating a large number of empty arrays.
  • It should have little effect on editor tooling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions