Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Probably the most advanced TypeScript runtime reflection system" #31

Closed
marcj opened this issue Feb 14, 2022 · 7 comments
Closed

"Probably the most advanced TypeScript runtime reflection system" #31

marcj opened this issue Feb 14, 2022 · 7 comments

Comments

@marcj
Copy link

marcj commented Feb 14, 2022

Regarding such statements I'd be careful, because microsoft/TypeScript#47658 is more advanced in addition to emitting smaller JS code for runtime type information.

@Hookyns
Copy link
Owner

Hookyns commented Feb 14, 2022

Hi @marcj, welcome to my humble repository.
I've read your proposal few days ago, it is interesting. 🙂

I just don't get one thing. You have focus on bundle size and you want to create eg. C++ interpreter for that. How would it run in the browser? WebAssembly? Cuz if it is meant for servers, size of generated metadata is not important IMHO.
Sorry if I missed something, it's quite large proposal with a lot of materials...


Regarding such statements I'd be careful, because microsoft/TypeScript#47658 is more advanced in addition to emitting smaller JS code for runtime type information.

Well, according to your text, this reflection system is level 3, which is ,,Advanced reflection". Your words.

Size of generated metadata is something which can be easily changed. I'm designing plugin/middleware system for the transformer. Transformer creates JS objects with metadata (like it does now) and middleware can easily convert those metadata to anything (eg. bytecode). Runtime will parse that and we are good to go. Transformer does the same thing. Runtime does the same thing. Only metadata would be "compressed" somehow.

My "advanced" means exactly what the Features section in the README says.

  • You don't have to use any decorators and you can get type of classes/interfaces/whatever from ANY 3rd party package and that package does not have to count with that. This is one of the main features which throws majority of the other reflection systems behind.
  • You can get type of generic parameter (Which reflection system allows this? I didn't check them all, I don't even know them all, maybe there are some...),
  • you can get type of anything (classes, types, interfaces, primitives, unions, intersections, ...),
  • it is plain TypeScript, no schemas, no annotations nor decorators,
  • Type is quite rich (check the synopsis), there are properties, their types, modifiers, decorators with constant arguments; methods with generic types, constraints, parameters, constructors with all the overloads,.. almost everything you need and there is still more.
  • Of course circular references are no problem,
  • metadata can be generated into one file (configurable), that means you have library and you can look up the types and use dynamic imports to load the classes and work with them.
Eg. BE frameworks can find all the constructors by simple linq:
const controllers = Type.getAll().filter(type => type.isSubclassOf(getType<ControllerBase>()));

or find all methods decorated by @route("/path"):

const action = "/todo";

const decoratedMethods = Type.getTypes()
    .flatMap(type => type.getMethods().map(method => ({ method, type })))
    .filter(entry => entry.method.getDecorators().some(decorator => decorator.name == "route" && decorator.getParameters()[0].startsWith(action)));

for (let entry of decoratedMethods)
{
    const Ctor = entry.type.getCtor();
    const controllerInstance = new Ctor();
    controllerInstance[entry.method.name]();
}

That sounds pretty advanced to me.

I'd love to group the authors of all the reflection systems together. Create just the plain Reflection system standard. Type guards, validators, schemas,.. it is not Reflection. These are just things implemented via Reflection.

My system is plain Reflection and anybody can build all the mentioned things on top of this system. Out of the box. End-developers don't have to change a single line of code. Just enable reflection system, install packages they want and use it.

@marcj
Copy link
Author

marcj commented Feb 14, 2022

How would it run in the browser?

It runs in any JS environment, just like yours.

Size of generated metadata is something which can be easily changed

Only to a certain extend. If you want minimum rtti size, you need either a decompressor or bytecode interpreter, as outlined in the referenced issue. Additional emitted JS size related to type information is important especially in bigger projects.

Well, according to your text, this reflection system is level 3, which is ,,Advanced reflection". Your words.

Yes, the described level 3 is more than tst-reflect, as tst-reflect does not emit type functions, only the results, hence no dynamic type computation possible, which is required for resolving union generics in addition to extensive extends support (your isAssignableTo is only very rudimentary compared to what x extends y is really capable of, check for example a more complete implementation).

You can get type of generic parameter

You can't, see for example:

	function infer<T extends string|number>(v: T): Type
	{
		return getType<T>();
	}

	console.log(infer('abc'));
	console.log(infer(23));

        //or
	function infer<T extends string|number>(v: T): Type
	{
		class A {
			b!: T
		}
		return getType<A>();
	}

	console.log(infer('abc').getProperties()[0].type);

do not work.

A few other things that do not work while playing around with the code:

//prints warning and returns undefined
	type test<A extends 0[] = []> = `${A['length']}`;
	console.log(getType<test>());

	type StringToNum<T extends string, A extends 0[] = []> = `${A['length']}` extends T ? A['length'] : StringToNum<T, [...A, 0]>;
       
	console.log(getType<StringToNum<'100'>>());
       //small example from MongoDB filter structure
	type RegExpForString<T> = T extends string ? (RegExp | T) : T;
	type MongoAltQuery<T> = T extends Array<infer U> ? (T | RegExpForString<U>) : RegExpForString<T>;

	type QuerySelector<T> = {
		$eq?: T;
		$not?: QuerySelector<T>;
	}

	type FilterQuery<T> = {
		[P in keyof T]?: QuerySelector<T[P]>;
	};

	interface Product {
		id: number;
		title: string;
	}

        //getProperties()[1] is reported to be an union, which is wrong
	const type = getType<QuerySelector<Product>>();
//built-in classes are not supported
console.log(getType<Map<string, string>>());
console.log(getType<Set<string>>());
console.log(getType<Uint8Array>());
console.log(getType<Date>());
console.log(getType<ArrayBuffer>());
//other basic types are not support on getType()
console.log(getType<[string, number]>());
console.log(getType<[a: string, b: number]>());
console.log(getType<[string, ...number[]]>());

//no template literal support
console.log(getType<`a${number}`>());

//no literal support
console.log(getType<'abc'>());
//basic assignable not supported
	interface A {
		property: 'abc';
	}

	interface B {
		property: string;
	}

	type r = A extends B ? true : false;
	//should be true
	console.log(getType<A>().isAssignableTo(getType<B>()));
//global type functions not supported

	interface A {
		a: true;
		b: number;
	}
	console.log(getType<Partial<A>>().getProperties());
	console.log(getType<Readonly<A>>().getProperties());
	console.log(getType<Pick<A, 'a'>>().getProperties());
//getProperties() shouldn't be empty
	class MyClass {
		constructor(public a: string) {
		}
	}

	console.log(getType<MyClass>().getProperties()[0]);

you can get type of anything

Not true, see:

  • Runtime type infer does not support Set, Map, typed arrays, etc.
  • Template literals not supported
  • No index signatures

More advanced stuff:

  • No type decorator support (for example for adding validator/ORM information and read them easily without fiddling around with intersections)
  • No parent relationship in nested types (property signature to object literal for example, or parameter to method)

Just a quick look at the code: "advanced" means the basic things need to be supported first, which sadly aren't.

@marcj
Copy link
Author

marcj commented Feb 14, 2022

Transformer creates JS objects with metadata (like it does now) and middleware can easily convert those metadata to anything (eg. bytecode). Runtime will parse that and we are good to go.

That's not needed. Bytecode can be generated more efficiently using the AST directly. No need to have a library in-between.

Create just the plain Reflection system standard. Type guards, validators, schemas,.. it is not Reflection.

The logic in type guards are inherently required to support resolving generic types. They are part of a TS reflection system, or otherwise the reflection system is not complete.

All of that is supported in microsoft/TypeScript#47658 plus much more. So I think the statement about "most advanced runtime reflection system" should be removed.

@Hookyns
Copy link
Owner

Hookyns commented Feb 15, 2022

Runtime generic
This works.

import { getType, Type } from "tst-reflect";

function infer<T extends string|number>(): Type
{
	return getType<T>();
}

console.log(infer<'abc'>());
console.log(infer<23>());

or

import { getType, Type } from "tst-reflect";

function infer<T>(): Type
{
	return getType<T>();
}

interface Ifce {}
class Klass {}

console.log(infer<Ifce >());
console.log(infer<Klass>());

Type infer from passed arguments is just not implemented yet. Nobody needed it yet.
And still, this base usage is something nobody has. This is advanced feature.

@marcj
Copy link
Author

marcj commented Feb 15, 2022

Runtime generic
This works.

That has nothing to do with runtime generics. Runtime is if the type is inferred from an actual runtime variable, like almost all generics are used. It's one of the basic TS feature to automatically resolve the generic type parameter. With your static generic implementation you have to manually annotate all generic parameters in order to get the runtime type.

And still, this base usage is something nobody has

I just pointed out that actual runtime generic parameters are supported in the referenced link.


I just saw that the following code is generated for infer:

function infer(__genericParams__) {
        return (__genericParams__ && __genericParams__.T);
    }

This breaks a lot of code as arguments is changed. Any code with dynamic parameters that use arguments will break this way. (for example if ES5 is the target). Also infer.length yields an unexpected/wrong value.

@Hookyns
Copy link
Owner

Hookyns commented Feb 15, 2022

Well I'm not gonna react to everything so late..

Summary to everything:
You've just picked all the edge cases to throw it away. You are comparing "jung" package in alpha stage with something what is developed by 13 people several years. Congratulation, you are winner!

I still think that my concept is advanced. It looks the way Reflection should. Usage is much more advanced and user friendlier than deepkit/types. Sorry.

@marcj
Copy link
Author

marcj commented Feb 15, 2022

Don't get me wrong, I'm not here to bash you or your effort. I appreciate every contribution to runtime types, which includes your efforts. It sometimes comes across harsh when information is shared in an unemotional way.

I just pointed out that your claims are objectively wrong. Template literal, index signature, runtime generics are no edge cases. The user facing API of the referenced link is literally almost the same as yours with the Reflection classes, expect that it supports more TypeScript types and has no false claims. It's not about who wins what, I don't care about that. I only care about false claims being pointed out and corrected, where the first has now been done. Your false statements mislead people, which is not ok. Please stop that. It's up to you what you do with that information now and if you correct it.

@marcj marcj closed this as completed Feb 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants