Hookyns / tst-reflect

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

[Question] Comparison of Array types #85

Closed Andy-d-g closed 1 year ago

Andy-d-g commented 1 year ago

Describe the bug The lib do not detect Array of interface

The code causing the problem

interface A {
  v: number
}
const value: A[] = [{ v: 1 }, { v: 2 }, { v: 3 }];
console.log(getType(value).isAssignableTo(getType<A[]>()))

Expected behavior return true

Runtime (please complete the following information):

Additional context Do you know how I can solve my issue ?

Thank you :)

Hookyns commented 1 year ago

Hi @Andy-d-g,

The way you wrote it you reflect over runtime variable value, but it is some runtime value without type information. If you pass something to the getType(x) function, it tries to infer type from the runtime value. In this case, it is able to detect it is an array, but it doesn't know of what type. So both types are array, but they don't match because of type parameter of the Array (Array<A> vs Array<Unknown>).

Possible solution in case you (and the TS type checker) know the type of the value:

console.log(getType<typeof value>().isAssignableTo(getType<A[]>()));

If you don't know the type, you can do something like this:

const typeOfValue = getType(value);

console.log(
  typeOfValue.isArray() &&
    typeOfValue.getTypeArguments()[0].types[0].isAssignableTo(getType<A>())
);

Example: https://stackblitz.com/edit/tst-reflect-example-arrays?file=index.ts

Andy-d-g commented 1 year ago

Ok but in this case, how can I check the type of a "complexe" object with array of interface in it ?

interface Client {
  name: string;
}
interface Establishment {
  name: string;
  clients: Client[]
}
const establishment: Establishment = {
  name: "google",
  clients: [
    {name: "client1"},
    {name: "client2"}
  ]
};
console.log(getType(establishment).isAssignableTo(getType<Establishment>()));

This code will return false same if the object as the correct type. The issue come from the clients array

Hookyns commented 1 year ago

I'm sorry but I just told you that you cannot use the getType(establishment) but you have to use getType<typeof establishment>().

This returns true: getType<typeof establishment>().isAssignableTo(getType<Establishment>()).

This library works with static types. Don't use getType(something), it's meant for runtime values but only for classes and/or primitive types. It is technically impossible to get interface of random in-memory object. You have to use getType<SomeType>() or you have to compare it manually.

Hookyns commented 1 year ago

Tell me what you are trying to achieve. I think you sent simple examples of something you want to achieve but there is huge difference how to do it between this simple example and complex one.

For all of this simple cases you just have to use getType<typeof someVariable>() not getType(someVariable). That's your answer.

If you get some unknown data of type any as an argument of your function, it's imposible to get the type. If TypeScript don't know that, it's not possible to get the type of such parameter. (It must be known in your context or in caller's context)

If you have such unknown parameter, that's the case for the getType(someValueOfUnknownType) in case it is a class, because class holds the reference to its type. If it's not a class you should check it manualy by iterating over properties and comparing each of them. There is no other way. That's not just a problem of this library, it's just technically impossible; even C# cannot return type of, for example, JSON parsed to dynamic object such as JsonConvert.Deserialize<dynamic>("{.......}"). JsonConvert.Deserialize<dynamic>("{.......}").GetType() in C# returns dynamic, Object, Dictionary or something like that, it depends on the implementation. If the compiler and the programmer don't know the type, it's impossible.

Andy-d-g commented 1 year ago

Your response is clear, I can't do what I want.

Thx

Hookyns commented 1 year ago

For example, this is valid example too.

mylib.ts


interface Client {
name: string;
}

interface Establishment { name: string; clients: Client[] }

function letMeDoSomethingWithYourType(value: TType) { return getType().isAssignableTo(getType()); }


> moduleOfSomebodyElseUsingYourLib.ts
```typescript
import { Establishment, letMeDoSomethingWithYourType  } from "./mylib.ts";

const establishment: Establishment = {
  name: "google",
  clients: [
    {name: "client1"},
    {name: "client2"}
  ]
};

console.log(letMeDoSomethingWithYourType(establishment));

https://stackblitz.com/edit/tst-reflect-example-arrays-5xcjrb?file=index.ts

Hookyns commented 1 year ago

This is valid too...

import { Establishment, letMeDoSomethingWithYourType  } from "./mylib.ts";

const establishment: Establishment = fetch("...") as any;
// or const establishment = fetch("...") as Establishment ;
// or const establishment = JSON.parse("{ ..... }") as Establishment ;

console.log(letMeDoSomethingWithYourType(establishment));
Andy-d-g commented 1 year ago

It's valid because it's type as "Establishment". In my case, I received data but I don't know what type is it. So I need to check if the structure of the object match the interface structure.

const establishment = {
  name: 'google',
  clients: [{ name: 'client1' }, { name: 'client2' }],
  test: '',
} as Establishment;
console.log(letMeDoSomethingWithYourType(establishment)); // return true same if it's not

const establishment = {
  name: 'google',
  clients: [{ name: 'client1' }, { name: 'client2' }],
};

console.log(letMeDoSomethingWithYourType(establishment)); // return false same if it's true

I've understand that I can't do what I want, i need to dig into the object which has array to check each value in it.

I'm right ?

Hookyns commented 1 year ago

Yes.

You have to do something like this:

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

const establishment: Establishment = {
  name: 'google',
  clients: [{ name: 'client1' }, { name: 'client2' }],
};

console.log(isMyArr([establishment, {}, undefined, 42]));

function isMyArr(value: any[]): value is Establishment[] {
  if (!(value instanceof Array)) {
    return false;
  }

  const estType = getType<Establishment>();

  for (let item of value) {
    if (!matchPropertiesOf(item, estType)) {
      return false;
    }
  }

  return true;
}

function matchPropertiesOf(item: any, type: Type): boolean {
  if (!item || item.constructor !== Object) {
    return false;
  }

  for (let prop of type.getProperties()) {
    if (!item.hasOwnProperty(prop.name) && !prop.optional) {
      return false;
    }

    const propVal = item[prop.name];

    if (prop.type.isObjectLike()) {
      return matchPropertiesOf(propVal, prop.type);
    }

    // TODO: check primitive types etc....
  }
}

It is something similar to how the getType(someUnknownValue).isAssignableTo(knownType) works, but it's naive implementation. getType(someUnknownValue) parse the object and create Type from parsed runtime value. Then it should be comparable by type.isAssignableTo(), but as I said,.. it's naive implementation.

https://github.com/Hookyns/tst-reflect/blob/cb6d9ffd292b150c1a11a5472e0bed2812f5c8d0/runtime/src/reflect.ts#L17-L72

Try to use getType(someUnknownRUntimeValue).isStructurallyAssignableTo(getType<KnownType>()), it should do what you need and what's possible to do. In case it fails you can play with it and create PR. I had no proper use case so it's not tested well, but it's the way how the unknown random runtime object should be compared to known Type. You cannot use isAssignableTo because it check types, inheritance etc. but isStructurallyAssignableTo should do base recursive check of properties and methods and some basic type checking.