Hookyns / tst-reflect

Advanced TypeScript runtime reflection system
MIT License
338 stars 11 forks source link

Null is usually not added to a variable's list of types #45

Open Irelynx opened 2 years ago

Irelynx commented 2 years ago

Hello.

I have tried to make some fields of class nullable, but can't get any information about allowed "nullable" behavior (same works with types, functions and interfaces too).

After a little investigation of the behavior, it turned out that the null type is added to field types in some cases. Here is an example code (ts) and "compiled" code (js):

import { NullLiteral } from "typescript";
// ...
class Test3 {
    testString1: string | null = null;
    testString2: null | string = null;
    testNull: null = null;
    testNumber: number | null = null;
    testUndefined: undefined | null = null;
    testDate: Date | null = null;
    testNullLiteral: NullLiteral | null = null;
    testBoolean: boolean | null = null;
    testAny: any | null = null;
    testInterface: Test2 | null = null;
    testPromise: Promise<string | number | null> | null = null;
    testMethod(arg: string | null): string | null {
        return arg;
    }
}
_ßr.Type.store.set(31, { n: "test3", k: 3, types: [
    _ßr.Type.store.wrap({ n: "string", k: 2, ctor: function () {return Promise.resolve(String);} }),
    _ßr.Type.store.wrap({ n: "number", k: 2, ctor: function () {return Promise.resolve(Number);} })
], union: true, inter: false });

_ßr.Type.store.set(99,  { n: "testDate", k: 3, types: [_ßr.Type.store.getLazy(99), _ßr.Type.store.wrap({ n: "null", k: 2 })], union: true, inter: false });
_ßr.Type.store.set(100, { n: "testNullLiteral", k: 3, types: [_ßr.Type.store.getLazy(100), _ßr.Type.store.wrap({ n: "null", k: 2 })], union: true, inter: false });
_ßr.Type.store.set(112, { n: "Promise", k: 2, args: [_ßr.Type.store.get(31)] });

_ßr.Type.store.set(95, { k: 1, n: "Test3", fn: "ts-api-test/test.ts:Test3#95", props: [
    { n: "testString1", t: _ßr.Type.store.wrap({ n: "string", k: 2, ctor: function () {return Promise.resolve(String);} }), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testString2", t: _ßr.Type.store.wrap({ n: "string", k: 2, ctor: function () {return Promise.resolve(String);} }), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testNull", t: _ßr.Type.store.wrap({ n: "null", k: 2 }), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testNumber", t: _ßr.Type.store.wrap({ n: "number", k: 2, ctor: function () {return Promise.resolve(Number);} }), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testUndefined", t: _ßr.Type.store.wrap({ n: "null", k: 2 }), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testDate", t: _ßr.Type.store.get(99), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testNullLiteral", t: _ßr.Type.store.get(100), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testBoolean", t: _ßr.Type.store.wrap({ n: "boolean", k: 2, ctor: function () {return Promise.resolve(Boolean);} }), am: 2, acs: 0, ro: false, o: false },
    { n: "testAny", t: _ßr.Type.store.wrap({ n: "any", k: 2 }), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testInterface", t: _ßr.Type.store.get(88), am: 2, acs: 0, ro: false, o: false }, 
    { n: "testPromise", t: _ßr.Type.store.get(112), am: 2, acs: 0, ro: false, o: false }
], meths: [
    { n: "testMethod", params: [
        { n: "arg", t: _ßr.Type.store.wrap({ n: "string", k: 2, ctor: function () {return Promise.resolve(String);} }), o: false }
    ], rt: _ßr.Type.store.wrap({ n: "string", k: 2, ctor: function () {return Promise.resolve(String);} }), tp: [], o: false, am: 2 }
], ctors: [{ params: [] }], ctor: function () {return Promise.resolve(Test3);} });

As you can see, only types for testNull, testDate, testNullLiteral are shown as they should.


Also, is the behavior of testDate correct?

In testDate recursive reference specified to type testDate (_ßr.Type.store.getLazy(99))


tst-reflect: 0.7.5 tst-reflect-transformer: 0.9.10

Hookyns commented 2 years ago

TY @Irelynx for the issue. I'll look into it.

Yes, testDate is ttly wrong.

Hookyns commented 2 years ago

This is caused by the strictNullChecks tsconfig option. It is disabled by default and when it is disabled, you can assign null into many types.

Such as:

class Foo {
    testString1: string = null;
    testNumber: number = null;
    testBoolean: boolean = null;
}

const x = new Foo();
x.testString1 = null;
x.testNumber = null;
x.testBoolean = null;

That is valid TS code with default tsconfig (with strictNullChecks disabled).

So when you have

class Foo {
    testString1: string | null = null;
    testNumber: number | null = null;
    testBoolean: boolean | null = null;
}

it is the same type, cuz those nulls are stripped off. Compiler throws it away with disabled strictNullChecks.

Solution is to enable the strictNullChecks option.

There is a way how to get that information anyway (that there is union with null), but it is quite complicated and not reliable.

Hookyns commented 2 years ago

This must be handled so the reflection keeps standard behavior, no matter what the strictNullChecks option is. So with strictNullChecks: false (default) every type will be union with null.

So not just fooProp: string | null; will be string | null but fooProp: string; will be string | null too.

But this has big impact and it is quite complicated to implement this into currect version so it will be in the next version.

Irelynx commented 2 years ago

Thanks a lot for your investigation!

I will try change the tsconfig and test my code as soon as I have time for it.