microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.17k stars 12.38k forks source link

Discussion: (Reflective) Type Model #3628

Closed christyharagan closed 1 year ago

christyharagan commented 9 years ago

Hi all,

We currently have a AST model for TypeScript which is useful for compilers, editors, and linters.

However, it would be extremely useful to have a type model, a la Java's reflective model. There are a huge number of use-cases supporting this, but I will list a few here:

A good example of what could be achieved with this, is something like the Spring platform for Java where our applications are built as decorated components and most of the boilerplate code is abstracted away by the platform thanks to reflection and decorators.

I have a "first stab" at implementing such a thing: typescript-schema. It's not feature complete and the design isn't finished either. But hopefully it gives a feel for what a reflective model could look like.

Is this something the TypeScript team would be interested in having as a first class citizen?

Artazor commented 7 years ago

I'd propose a special decorator on interfaces

export const MyInterface = Symbol("MyInterface")

@reflectAs(MyInterface)               // here MyInterface is the constant
export interface MyInterface {        // that will be emmited as a reflected type 
      a: number;
      b: boolean;
}

It would be extremely simple and efficient solution for DI

koroandr commented 7 years ago

@Artazor The problem is interfaces are not compiled to any real js code, but decorators are. What should be the output of code you suggested?

I think, that's the reason why it should be keyword, not decorator.

remojansen commented 7 years ago

@Artazor @koroandr I think the best solution is to add a compiler option, something like the current --emitDecoratorMetadata but --emitInterfaceMetadata or something similar but I also think that --nominalTyping which would disable structural typing. I assume this is quite complicated but I'm not a compiler guy so I'm not sure about it...

I know that the structural type system was designed to facilitate interoperability with JavaScript libraries but disabling structural typing would enable some awesome tools just like the strict null checks mode does.

koroandr commented 7 years ago

@remojansen I believe that may cause some overhead, because in fact you wouldn't need reflection metadata for every interface you create, just for injectable ones (in case of DI), so that will produce a lot of wasteful code when compiled to js. That's why I suggested to use some keyword, like so:

export reflexed interface MyInterface {
    a: number;
    b: boolean;
}
thomas-darling commented 7 years ago

Can we please just have an --emitInterfaces option and be done with this? This discussion has been ongoing for so long now, and it really should not be that complicated. There is a crystal clear and highly desirable use case for this, and we just need the compiler to emit something that we can reference at runtime - and I seriously doubt this would cause any measurable overhead, so no need to complicate things with new keywords or decorators.

So when I write something like this in foo.ts:

export interface Foo {}

The compiler could just emit code similar to what would be emittted if it was written like this:

export abstract class Foo {}

(but of course, it is still an interface, and therefore still can't be used with instanceof, etc.)

That way, when I do something like this in bar.ts, the metadata emitted for the foo constructor parameter would reference the imported Foo - instead of always referencing Object as it currently does.

import {Foo} from "./foo";

@autoinject
class
{
    constructor(foo: Foo) {}
}

Done, problem solved - The metadata now contains a reference to something representing the imported interface, which can then be used as a key for dependency injection. Whether that something is then an Symbol, Object, or Function really doesn't matter - it's a reference, and that's all that matters.

skyrpex commented 7 years ago

I'd love to see this happening too. I find it essential for evolving the language and creating better software.

studds commented 7 years ago

I understand that metadata supports only names with a value attached to them (i.e. classes) and builtins. Is it prohibitively difficult to extend support to native objects like Date?

aluanhaddad commented 7 years ago

Can we please just have an --emitInterfaces option and be done with this?

This would be problematic in that it would change the meaning of existing code. Once you start reifying interfaces arbitrarily you will get variable collisions and variable shadowing in all sorts of unexpected places. Even referencing lib.d.ts would cause code to fail.

interface Element {
}

takes on a whole different meaning.

tarruda commented 7 years ago

Similar idea: #12605

atrauzzi commented 7 years ago

Based on my own scenario and what I'm seeing here, it seems like the commonality is that this functionality is only ever desired for classes and interfaces.

A few suggestions and caveats have surfaced in this discussion, but a way forward seems apparent as I read:

Two years for a feature that's generated an awful lot of chatter. I would file this ticket under "more impactful than it's being given credit for".

https://github.com/inversify/InversifyJS/issues/534

✏️

ghost commented 7 years ago

We also had a problem with DI and interfaces and made up two sollutions.

  1. Using interface as a var
    
    /// ISomeService.ts
    export interface ISomeService {}
    // Now ISomeService is also an empty object emmited as ISomeService = {};
    export module ISomeService { let ISomeService: void; }

/// SomeService1.ts import { ISomeService } from "./ISomeService";

// And could be used as a var @Injectable(ISomeService) export class SomeService1 implements ISomeService {}


2. Decorating an interface

/// ISomeDBType.ts export class ISomeDBType { // Since ISomeDBType is a class we could apply decorators to its properties, // but TS will emmit empty function/class. @PrimaryKey public ID: number; }

/// ISomeValidationType.ts export class ISomeValidationType { @Validate("number") public ID: number; }

/// SomeType.ts import { ISomeDBType } from "./ISomeDBType"; import { ISomeValidationType } from "./ISomeValidationType";

// We also have a special "partial" decorator which copies all metadata from ISomeDBType and // ISomeValidationType to SomeType and lets us keep classes clean (since we heavily rely on // metadata for almost everything). @Partial(ISomeDBType) @Partial(ISomeValidationType) // TS can use class as an interface, so everything is fine. class SomeType implements ISomeDBType, ISomeValidationType { private id: number; public get ID(): number{ return this.id; } public set ID(ID: number) { this.id = ID; } }

goloveychuk commented 7 years ago

For those who interested: I wrote little library, which uses customTranformers api. It emits some basic classes and props metadata, including generic arguments and union types. Looks like typescript team doesn't want to include such functionality into the main repository (at least for now). But same can be achieved using transformers api So, this is proof of concept, comments are welcome. https://github.com/goloveychuk/tsruntime linked with #3015

kimamula commented 7 years ago

I agree that custom transformers can be a solution.

The following are the examples of what you can do with custom transformers.

import { enumerate } from 'ts-transformer-enumerate';

type Colors = 'green' | 'yellow' | 'red';
const Colors = enumerate<Colors>();

console.log(Colors.green); // 'green'
console.log(Colors.yellow); // 'yellow'
console.log(Colors.red); // 'red'
import { keys } from 'ts-transformer-keys';

interface Props {
  id: string;
  name: string;
  age: number;
}
const keysOfProps = keys<Props>();

console.log(keysOfProps); // ['id', 'name', 'age']
bcherny commented 7 years ago

@christyharagan Also see:

goloveychuk commented 7 years ago

Can smbd help me? https://github.com/goloveychuk/tsruntime/blob/master/src/transformer.ts#L58 There I'm emitting reference type. But if this type was imported from another module, and not used anywhere on this file, typescript removing it (as unused). So I need somehow mark this type as used. How can I do this? What mechanisms checking this? ImportTracker? codefixes/unusedIdentifierFixes?

upd: smth found, looks like it's isReferencedAliasDeclaration

rbuckton commented 7 years ago

Yes. Import elision is handled in the checker. isReferencedAliasDeclaration pulls information from the checker to indicate whether the import was referenced.

goloveychuk commented 7 years ago

Just some thoughts. If typescript team don't going to make enhanced metadata emitting inside typescript codebase, it would be great boost (for custom libraries) if you'll move metadata logic to the external library, which uses only public api. It will help to understood, what api is needed and currently not available. And it will really help people to write solid custom library for metadata emitting. E.g. for now https://github.com/Microsoft/TypeScript/blob/2150a7796b0dfe83c5286755410e0fd1646ed86c/src/compiler/transformers/ts.ts#L1806 EmitResolver api is not available. Thanks.

lilezek commented 7 years ago

Is already there any solution for emitting better metadata? With better I mean metadata with correct complex types or metadata about interfaces (which are known to disappear after transpiling).

goloveychuk commented 7 years ago

You can look at my tsruntime library. But it doesn’t emit info for interfaces

On Aug 12, 2017, 3:15 PM +0300, lilezek notifications@github.com, wrote:

Is already there any solution for emitting better metadata? With better I mean metadata with correct complex types or metadata about interfaces (which are known to disappear after transpiling). — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or mute the thread.

lilezek commented 7 years ago

@goloveychuk Well, as I've seen no solution for what I want to achieve I've made two libraries: The first one generates a TypeScript file including some metadata, just like tsruntime but without the need of placing decorators. It also aims to emit interface metadata in the near future. https://github.com/lilezek/awesome-metadata

The second one uses generated TypeScript metadata to implement the functionality that GSON offers in Java, for serializing and deserializing classes. https://github.com/lilezek/tson

I hope someone finds these projects useful.

pie6k commented 6 years ago

Almost 3 years passed and there is no kind of 'official' statement from Typescript team about this issue.

First-class Reflective features would unlock many possibilities as described in issue description.

Many third-party libraries have been created and they're doing good job, but in fact, it's dividing the way things can be achieved with typescript. First class support would avoid having several standards of doing something.

As this issue is important, such standards will eventually emerge if there will be void in this area. I consider it bad thing because community fragmented by the way of doing something important would slow down development and stability of language in real world scenarios.

Also I think that even if Typescript team have no plans for implementing it or finds it unnecessary or low-priori, it'd be valuable to have some information about it.

Thanks 👍

goloveychuk commented 6 years ago

I'm pretty sure that typescript team will not implement reflection support. https://github.com/Microsoft/TypeScript/issues/21479#issuecomment-361685802 Even if they implement more powerful (then now we have) reflection for classes (which have runtime representation), they will not make this for interfaces (which some developers want). But it's not a big deal, since it could easilly be imlemented via plugins. The only thing is needed from Typescript team is to give us an option to have in `tsconfig.json'

{
    plugins: ["awesomePlugin"]
}

and in 'awesomePlugin.ts'

export default {
      initialize(program: Program) {}
      emit(){}
}

Which is pretty similar to customTransformers api, but with access to Program and with ease installation (no mess with typescript loaders configs). And this, looking at ts source code, pretty easy to implement.

pcan commented 6 years ago

Many third-party libraries have been created and they're doing good job, but in fact, it's dividing the way things can be achieved with typescript. First class support would avoid having several standards of doing something.

As this issue is important, such standards will eventually emerge if there will be void in this area. I consider it bad thing.

I completely agree with you. The most dangerous thing is not the lack of reflection, but the fragmentation of the community-built solutions.

they will not make this for interfaces (which some developers want).

I did this two years ago in my reflec-ts fork of the official compiler, and with a very low effort, too. I repeat: we just need a standard track for this topic, otherwise in the future many of the libraries/frameworks produced by the community will be very fragmented and incompatibilities will raise, consequently.

EisenbergEffect commented 6 years ago

I'd be happy to write this code by hand...

const IFoo = Symbol('IFoo');
interface IFoo {
  bar();
}

If using emitDecoratorMetadata in this scenario...

export class Baz {
  constructor(foo: IFoo) {}
}

...actually generated design metadata with the IFoo symbol value in it, rather than Object. To me, this seems like a bug, since everywhere else, the compiler seems to understand when to use the interface type vs. when to use the constant. But when it emits design data, it does it incorrectly.

As an FYI, this is what we actually do today:

export const IFoo = DI.createInterface('IFoo');
export interface IFoo {
  ...
}

export class Bar {
  constructor(@IFoo foo: IFoo) { ... }
}

Our createInterface helper creates a decorator that will push itself into the metadata so it can be used as a key by the DI. This is a workaround that is needed because of the emit issue I described above. I'd prefer to just write this...

@autoinject
export class Bar {
  constructor(foo: IFoo) { ... }
}

(Would be better to not need the autoinject decorator to turn metadata emit on, but I'd be happy to just see the interface/value emit fixed.)

While this fix wouldn't satisfy every need on this thread, it's a reasonable non-breaking change that would open up the DI scenarios a little more.

backbone87 commented 6 years ago

Interfaces should have been called Shapes, that would have prevented a lot of confusion and kept room for actual interfaces known from other languages.

Maybe its worth introducing a nominal typing variant of interface? Possible name could be contract or, to avoid the confusion with other languages (read: interfaces are shapes and contracts are interfaces), a subqualifier for interfaces: nominal interface, runtime interface

When using Symbols for runtime transpilation then the code representing the interface at runtime must be moved to a separate module to avoid side effects (because importing only an interface from another would result in this import being removed when transpiled).

A possible way to represent interfaces at runtime:

/** INTERFACE RUNTIME */
const Implements = Symbol('Typescript Interface Implementations');

function Interface(desc?: string) {
    const symbol = Symbol(desc);
    const instance = function() {} as any;
    Object.setPrototypeOf(instance, Interface.prototype);
    Object.defineProperty(instance, Symbol.hasInstance, {
        value: (other: any) => {
            if (!other.constructor || !other.constructor[Implements]) {
                return false;
            }

            return other.constructor[Implements].includes(instance);
        }
    });
    Object.defineProperty(instance, Symbol.toPrimitive, { value: () => symbol });
    Object.defineProperty(instance, 'toString', { value: () => desc || '' });
    return instance;
};
Interface.prototype = Object.create(Function.prototype);
/** INTERFACE RUNTIME */

const MyInterface = Interface('b::MyInterface'); // added by transpilation
interface MyInterface {
}

class Other implements MyInterface {
    static readonly [Implements] = [MyInterface]; // added by transpilation
}

const implementor = new Other();
const bindings = {
    [MyInterface]: implementor,
}

console.log(MyInterface); // [Function: instance]
console.log(typeof MyInterface); // function
console.log(MyInterface instanceof Interface); // true
console.log(MyInterface instanceof Function); // true
console.log(MyInterface instanceof Object); // true
console.log(MyInterface instanceof Symbol); // false
console.log(MyInterface.toString()); // b::MyInterface
// console.log(MyInterface + ''); // Error
console.log(implementor instanceof Other); // true
console.log(implementor instanceof MyInterface); // true
console.log(bindings); // { [Symbol(b::MyInterface)]: Other {} }

Object.getOwnPropertySymbols(bindings).forEach((k) => {
    console.log(k); // Symbol(b::MyInterface)
    console.log(k == MyInterface); // true
    console.log(MyInterface == k); // true
    console.log(k === MyInterface); // false ! some sadness, but we can live with that
})
daweedm commented 5 years ago

Any news about official support from the TS team?

philipp-sorin commented 5 years ago

Maybe it can help someone, I found this lib (https://github.com/dsherret/ts-simple-ast), looks very promising for compile-time reflection

YePpHa commented 5 years ago

I've been playing around with custom transformers in trying to solve the problem with dependency injection and interfaces. I've been able to make a custom transformer and a simple dependency injection library to demonstrate the capabilities of my transformer. The transformer is able to do the following:

Example of how to use it:

interface IBird {
  canFly(): boolean;
}

class Crow implements IBird {
  public canFly() {
    return true;
  }
}

class App {
  constructor(bird: IBird) {
    if (bird.canFly()) {
      console.log("Bird can fly");
    } else {
      console.log("Bird can't fly");
    }
  }
}

const container = new Container();
container.bind(InterfaceSymbol<IBird>(), Crow);

container.resolve(App);

If you're interested in it you can go to https://github.com/YePpHa/ts-di-transformer for more information. Please note that it's mostly a proof-of-concept even though it's kind of working.

I think that custom transformers are the solution for this issue. It might be possible that there's other possible solutions. But so far I think they're the way to go.

igauravsehrawat commented 5 years ago

The only hope is this package. https://www.npmjs.com/package/reflect-metadata

NicolasDuran commented 4 years ago

Up! This would definitely profide a clean solution to parse/validate untrusted JSON input at runtime!

paleo commented 4 years ago

This would definitely profide a clean solution to parse/validate untrusted JSON input at runtime!

I suggest to try the TypeOnly checker. https://github.com/tomko-team/typeonly-checker

goloveychuk commented 4 years ago

You can also try https://github.com/goloveychuk/tsruntime

aenario commented 4 years ago

I have been playing with this idea for a few day and published a quick POC using a transformer. https://github.com/aenario/tsmirror. If you are interested, feel free to open an issue there to describe your usecase.

edbentley commented 4 years ago

For the "REST interface" use case, I've worked on a library I hope can solve it conveniently (requires a Babel setup at the moment): https://github.com/edbentley/ts-validate-type

cyraid commented 4 years ago

Hasn't anyone used Pascal? haha .. In Object Pascal there'd be a compiler flag to emit run-time type information for anything in the block.. I know it wouldn't work for Typescript to control what is emitted, but adding a keyword might work?


export reflect class SomeClass {
} // Class //

export class SomeOtherClass {
  public reflect someMethod(): void {
  } // Procedure //
} // Class //
const reflect someVar: string = "hi";
const metaData = Reflect.getMetadata("design:type", someVar);

Ideally there would be more information emitted as well.. Like array with type, Enum Type information (what is the Enum's name, and values), and interface implementation checking.

akutruff commented 3 years ago

Dear TS team,

I love you. You do amazing work. You are gods among mortals. You have brought JavaScript from the darkness, and given it the warm light of strong typing. Look upon us, the cowering meek masses, and understand that we live in the muck and mire of a world where we are doomed to endlessly crawl on our bellies through the alleys of Github seeking the the one true npm. We will forever wonder in the dark, until we have a type reflection model.

Serialization and validation without a reflective type system just doesn't work in the real world without endless boilerplate, or shiver, bespoke code generation from a schema file. The best solutions I could find are io-ts, zod, myzod, etc. The most courageous of us even resort to babel, but only to face the consequences. We have to declare our types in a round about library-bespoke way that is foreign to the uninitiated, and the libraries aren't even able to support all the wonderful type magic you work so hard to provide.

This is a case, where we urge you to take a step back and holistically look at what TypeScript users have to do to in the majority of modern projects, and list them out in priority order. The decision of how to tackle serialization, and what actually has to be done to have TypeScript still work with it is at the top of the list. Even if you still don't want to support reflection, please write a detailed doc, and put it in the official TypeScript documentation that explains how TypeScript users should tackle this problem or what library you recommend, so we don't have endlessly reinvent the wheel.

Second, and maybe even more importantly is type based dispatch. Redux actions need a discriminator property such as type: 'incrementCounter' Without runtime type information for the string literal, we resort to having to have the interface as a generic argument, and the string literal for the discriminator explicitly declared over and over. The moment we rename the type, the string literal discriminator needs to be renamed too, and the tools can't deal with it for us. The official Redux Toolkit's createAction does the best it can, but then their concept of "matchers" are now everywhere in your code base. This is because it's impossible to make a map of discriminator value to a TypeScript type to perform the downcast that you need for any message based dispatch map. Please add a way to automatically declare a property on an interface with the discriminator set to a string literal, and a casting map to disambiguate.

Side note, please don't solve this with decorators. A lot of us want to use interfaces. Decorators, like C# attributes are so coupling, and we can't add them to types from other libraries. A higher order function that's known to the compiler like: typescript.generateRuntimeType<T>() with options for discriminators etc. means it would even work with external libs.

Last thing. If you do this, I will send you cake. If it's in TypeScript 4.3, there will be ice-cream.

Love, A humble disciple

Edit: Pinging Zeus himself: @ahejlsberg

alexweej commented 3 years ago

Thanks for those links @akutruff. Since I discovered io-ts a few weeks ago, I discovered so many places where I can and should use it to remove custom boilerplate while increasing safety. It's very unfortunate to have to learn at least TWO wildly different syntaxes for defining types, in order to be a TypeScript programmer.

sinclairzx81 commented 3 years ago

@akutruff I really did enjoy reading your comment :) It's probably been suggested many times before, but I do wonder if its just possible to have TypeScript emit a raw JSON schema literal object from a special kind of interface or type definition. Something like..

schema Foo {
   x: string,
   y: number,
   z: boolean
}
function test(foo: Foo) {
    JSON.validate(Foo, foo) 

    console.log(Foo.properties.x.type) // string
    console.log(Foo.properties.y.type) // number
    console.log(Foo.properties.z.type) // boolean
}

Where in the above example, Foo is seen as a TS type when used as a annotation, but seen as a JSON schema object when used in code. One thing I noticed about TypeScript is that it's already possible to share a name (Foo in this case) as a type or variable which TS is able to infer appropriately through usage. For reference, I maintain this project where I use this feature quite often.

type Foo = Static<typeof Foo> // <--+ 
                              //    |--- Note: TypeScript allows the Foo to be both variable and type.
const Foo = Type.Object({     // <--+
   x: Type.String(),
   y: Type.Number(),
   z: Type.Boolean()
})

function test(foo: Foo) {    // Seen as a type { x: string, y: number, z: boolean } when annotated.
    JSON.validate(Foo, foo)      // Seen as a json schema object when used explicitly.

    console.log(Foo.properties.x.type) // string
    console.log(Foo.properties.y.type) // number
    console.log(Foo.properties.z.type) // boolean
}

I guess for the purposes of runtime data type validation, JSON schema might be a pretty good emit target for TypeScript as it wouldn't need to invent it's own validation logic (AJV is pretty good and tied to JSON schemas standard / drafts). However, as JSON schema doesn't provide anything to validate or derive method signatures, I'm not sure if it ticks everyones boxes (with nods to .NET System.Reflection and all the goodness that brings). But JSON schema is probably the closest thing TS could get to emitting a reflective object type without implementing a full blown reflection system (which is most likely far far out of scope).

In terms of what's emitted to JavaScript, perhaps something like the following.

schema Foo {
   x: string,
   y: number,
   z?: boolean
}

// would emit as

const Foo = {
  type: 'object',
  properties: {
    x: { type: 'string' },
    y: { type: 'number' },
    z: { type: 'boolean' }
  },
  required: ['x', 'y']
}

Also, side note. I'm pleased this discussion thread is still active after all these years :D

Quick Update: For reference, the following is how TS types could potentially map over to JSON schema.

┌─────────────────────────────┬─────────────────────────────┐
│ TypeScript                  │ Emit to JavaScript          │
├─────────────────────────────┼─────────────────────────────┤
│ type T = any                │ const T = { }               │
├─────────────────────────────┼─────────────────────────────┤
│ type T = unknown            │ const T = { }               │
├─────────────────────────────┼─────────────────────────────┤
│ type T = string             │ const T = {                 │
│                             │    type: 'string'           │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = number             │ const T = {                 │
│                             │    type: 'number'           │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = boolean            │ const T = {                 │
│                             │    type: 'boolean'          │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = null               │ const T = {                 │
│                             │    type: 'null'             │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = string             │ const T = {                 │
│ (as regex)                  │    type: 'string',          │
│                             │    pattern: 'foo'           │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = 'foo'              │ const T = {                 │
│                             │    type: 'string',          │
│                             │    const: 'foo'             │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = number[]           │ const T = {                 │
│                             │    type: 'array',           │
│                             │    items: {                 │
│                             │      type: 'number'         │
│                             │    }                        │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = {                  │ const T = {                 │
│      [key: string]          │    type: 'object'           │
│ } : number                  │    additionalProperties: {  │
│                             │      type: 'number'         │
│                             │    }                        │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = {                  │ const T = {                 │
│    name: string,            │   type: 'object',           │
│    email: string            │   properties: {             │
│ }                           │      name: {                │
│                             │        type: 'string'       │
│                             │      },                     │
│                             │      email: {               │
│                             │        type: 'string'       │
│                             │      }                      │
│                             │   },                        │
│                             │   required: [               │
│                             │      'name',                │
│                             │      'email'                │
│                             │   ]                         |
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = [string, number]   │ const T = {                 │
│                             │    type: 'array',           │
│                             │    items: [                 │
│                             │       {                     │
│                             │         type: 'string'      │
│                             │       }, {                  │
│                             │         type: 'number'      │
│                             │       }                     │
│                             │    ],                       │
│                             │    additionalItems: false,  │
│                             │    minItems: 2,             │
│                             │    maxItems: 2,             │
│                             │ }                           |
├─────────────────────────────┼─────────────────────────────┤
│ enum Foo {                  │ const T = {                 │
│   A,                        │    enum: [0, 1]             │
│   B                         │ }                           │
│ }                           │                             │
│                             │                             │
│ type T = Foo                │                             │
│                             │                             │
├─────────────────────────────┼─────────────────────────────┤
│ type T = string | number    │ const T = {                 │
│                             │    anyOf: [{                │
│                             │       type: 'string'        │
│                             │    }, {                     │
│                             │       type: 'number'        │
│                             │    }]                       │
│                             │ }                           │
├─────────────────────────────┼─────────────────────────────┤
│ type T = {                  │ const T = {                 │
│    a: string                │   allOf: [{                 │
│ } & {                       │     type: 'object',         │
│    b: number                │     properties: {           │
│ }                           │        a: {                 │
│                             │          type: 'string'     │
│                             │        }                    │
│                             │     },                      │
│                             │     required: ['a']         │
│                             │   }, {                      │
│                             │     type: 'object',         │
│                             │     properties: {           │
│                             │       b: {                  │
│                             │         type: 'number'      │
│                             │       }                     │
│                             │     },                      │
│                             │     required:['b']          │
│                             │   }]                        │
│                             │ }                           │
└─────────────────────────────┴─────────────────────────────┘
rhyek commented 3 years ago

After four years of using TS professionally and loving it due to the productivity it offers, this is one the one feature I hope gets implemented some day. Having type information at runtime would be a god-send.

akutruff commented 3 years ago

https://www.typescriptneedstypes.com

Github

After reading this thread, I gathered all the projects listed, and the ones I've found from my searches. The goal is to centrally track how this is affecting us as a community to give empirical data on the impact to the TypeScript team.

I included all the project links in this thread. Please file an issue or send a PR to add to the list of attempted solutions. If your project is affected by this problem, or simply want to include a link to your GH account, I'll add those too. Please ask colleagues about their experience as well. All contributions welcome.

EricRabil commented 3 years ago

If the TypeScript team is just gonna let this die can they at least give us the necessary plugin APIs so we can do it ourselves?

I understand the resistance to fragmentation but it's happening anyways. Put it behind a flag, document that it may break interoperability (as other options do), and leave it at that.

I also want to note that I think TypeScript's ideology of not implementing certain features to prevent fragmentation creates more fragmentation. If TypeScript doesn't adopt a high-demand feature, then there's going to be dozens of libraries created (as we are seeing here) that have different strategies, APIs, and functionality, trying to fill the gap.

marcj commented 2 years ago

I've created a proposal and implementation for full runtime types with a reflection API: https://github.com/microsoft/TypeScript/issues/47658. It's not trivial, but I think has some nice and advanced features and characteristics (like small footprint of emitted type information) worth considering. Supporting all type expressions of TypeScript in runtime is a my personal desire, so its supported. Would appreciate your thoughts on this. If you like it, please leave a 👍 at the issue so the TypeScript team sees the demand.

rezonant commented 2 years ago

I have been working on an external solution as well, though I've been quite busy with other things it is still very much something I want to bring to release quality, and would love to collaborate- see https://github.com/rezonant/typescript-rtti

As for doing this directly within Typescript, the issue is complicated in that decorators are not in stage 3, and the inclusion of decorators and runtime type metadata runs afoul of the project goals in that Typescript wishes not to add runtime functionality beyond downlevelling standardized Javascript features.

Ultimately the best way to solve this is to build external solutions and work to get a community consensus on adoption and mechanism. Those external solutions must be engineered with an eye toward inoperability because it will be a long time before one solution rises to the position of a defacto standard.

Hookyns commented 2 years ago

I've made my own reflection system too => tst-reflect. And then I've built the Dependency Injection of top of that, see.

tst-reflect is based on custom transformer and it generates metadata about types, accessible by getType<SomeType>(): Type function. Instance of Type class is returned which contains quite a lot of methods to work with types. All the types inside/out are instances of Type.

Generic types supported! It works with quite complex types, unions, intersections, enums, details about interfaces and classes (constructors, properties, methods, parameters, generic types, decorators,.. ) and much more.

Usage is inspired by C# reflection.

Here is REPL with example. Runtime Type, even runtime generic

Simplified version of that REPL.


function printClassInfo<TType>()
{
const type = getType<TType>(); // <<== Here is the generic type used!
if (!type.isClass())
{
    return;
}

console.log("class " + type.name);
console.log("full type identifier: " + type.fullName);

const properties = type.getProperties();
const methods = type.getMethods();

console.log("Properties");
console.log(
    properties.map(prop => 
        `${AccessModifier[prop.accessModifier]} ${Accessor[prop.accessor]} ${prop.name}: ${prop.type.name}
    ).join("\n")
);

console.log("Methods");
console.log(
    methods.map(method => AccessModifier[method.accessModifier] + " " + method.name
        + "("
        + method.getParameters().map(param => param.name + ":" + param.type.name).join(", ")
        + "): " + method.returnType.name
        + (method.optional ? " [optional]" : "")
    ).join("\n")
);

}

class Bar { foo: string; bar: any; toSomething(): void; }

printClassInfo();

EisenbergEffect commented 2 years ago

Perhaps those in the community who have built or are working on reflection systems could collaborate together on a shared protocol. If TS doesn't want to bake this in but there's a community protocol that is implemented by the various reflection provider libraries/tools, then other libraries and frameworks could be written in such a way as to work with any compatible implementation.

This is one of the things we're currently doing in the web components world. There are a number of libraries for building web components and a number of W3C standards related to that. But there are features not covered by the standards (and probably never will be) that we want our various libraries to be able to interop around. So we meet to design community protocols and gain consensus around those.

rezonant commented 2 years ago

Yes, there are so many implementations that differ and are incompatible, and yet none that are dominant for one reason or another. Perhaps we can set up a working group to come to some base consensus on what a shared metadata format should look like and what it's goals and nongoals should be.

rezonant commented 2 years ago

Question would be where to start the process -- @akutruff Your existing repository could be a good place to organize these efforts, or we could create a new github org to host such a project. The primary deliverable would be a specification for the metadata format, preferable one that is extensible and I think we should reach out to all of the folks who have built their own solutions and invite them to participate.

EDIT: We could also build a reference implementation of course

akutruff commented 2 years ago

@EisenbergEffect I appreciate the sentiment. I really do. The issue is that we don't have what we need to build what we need. We are walled off. No amount of collaboration will change this. There are really good efforts, but we all hit the same wall for the same reasons. The type information is in the compiler, ready to be emitted. That's the source of truth, and any effort to build without it will result in the same dead-ends we get today. tsc --emit-types --emit-ast into blobs of json is the basic needand we can take it from there, but it ain't happening. We don't have that in a first class fashion, and we will always have to jump through the hoops that all the projects listed in the typescript-needs-types repository have to do. We're at a dead-end because no-one, rationally, wants to couple their projects to babel plugins, or the TypeScript-like fake DSL schema madness.

One of the great things about TypeScript, is that they built the language with IDE's as a first class citizen. In the past, IDE's needed to maintain their own parsers, AST's etc, and it was always double the work, caused fragility, and only a few select companies with the resources to do maintain these duplicate efforts could afford it. (e.g. DevExpress) The lack of type emission is precisely the same situation. We are walled off and forced to replicate what the compiler is already doing. It's not optional. All implementations must either contaminate the code, or put it their own meta-compiler tech.

I'm going to say the thing I shouldn't say which is why I don't make the big bucks: The effort is blocked by dogma without consideration of on-the-ground reality. The TypeScript team can emit types without violating the spirit of their rules, which I agree with - emit human readable JavaScript, and that's that. It makes 100% sense. However, the compiler emits type files right next to those JS files so the type information is preserved for editing experiences... They can emit machine readable type schemas too, and let us use them at runtime. It's so utterly arbitrary that I want to sit in a corner and stare out a window listening to Édith Piaf.

rezonant commented 2 years ago

I disagree that we have no path forward, transformers are totally capable of achieving the desired result and though their DX leaves something to be desired, they definitely provide the avenue that solves the problem, but the next problem is getting everyone on the same page and using compatible, or ideally the same, implementation

(see #14419 for a better transformer DX)

rezonant commented 2 years ago

However, the compiler emits type files right next to those JS files so the type information is preserved for editing experiences... They can emit machine readable type schemas too, and let us use them at runtime

I think that emitting next to the code instead of within the code limits the potential avenues they can be used (or at least heavily complicates it). Should I use fetch() to get the type information or fs.readFile? How do I know I'm on Node.js or Deno? Should this code need to know that just to get runtime type info?