tst-reflect icon indicating copy to clipboard operation
tst-reflect copied to clipboard

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

Open Irelynx opened this issue 3 years ago • 4 comments

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

Irelynx avatar Jun 23 '22 15:06 Irelynx

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

Yes, testDate is ttly wrong.

Hookyns avatar Jun 24 '22 07:06 Hookyns

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 avatar Aug 21 '22 11:08 Hookyns

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.

Hookyns avatar Aug 26 '22 21:08 Hookyns

Thanks a lot for your investigation!

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

Irelynx avatar Sep 01 '22 15:09 Irelynx