Hookyns/tst-reflect

[Question] Comparison of Array types

Closed this issue · 9 comments

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):

  • Node.js version: 18.9.1
  • TypeScript version: 4.8.3
  • Compiler: [e.g. ttypescript] : 1.5.13

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

Thank you :)

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

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

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.

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.

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

Thx

For example, this is valid example too.

mylib.ts

interface Client {
  name: string;
}

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

function letMeDoSomethingWithYourType<TType extends object>(value: TType) {
    return getType<TType>().isAssignableTo(getType<Establishment>());
}

moduleOfSomebodyElseUsingYourLib.ts

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

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));

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 ?

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.

export function getTypeOfRuntimeValue(value: any): Type
{
if (value === undefined) return Type.Undefined;
if (value === null) return Type.Null;
if (typeof value === "string") return Type.String;
if (typeof value === "number") return Type.Number;
if (typeof value === "boolean") return Type.Boolean;
if (value instanceof Date) return Type.Date;
if (value.constructor === Object) return ObjectLiteralTypeBuilder.fromObject(value);
if (!value.constructor)
{
return Type.Unknown;
}
if (value.constructor == Array)
{
const set = new Set<Type>();
// If it is an array, there can be anything; we'll check first X cuz of performance.
for (let item of value.slice(0, ArrayItemsCountToCheckItsType))
{
set.add(getTypeOfRuntimeValue(item));
}
const valuesTypes = Array.from(set);
const arrayBuilder = TypeBuilder.createArray();
if (value.length == 0)
{
return arrayBuilder
.setGenericType(Type.Any)
.build();
}
const unionBuilder = TypeBuilder.createUnion(valuesTypes);
// If there are more items than we checked, add Unknown type to the union.
if (value.length > ArrayItemsCountToCheckItsType)
{
unionBuilder.addTypes(Type.Unknown);
}
return arrayBuilder.setGenericType(unionBuilder.build()).build();
}
if (typeof value === "function" && (value.prototype == undefined || Object.getOwnPropertyDescriptor(value, "prototype")?.writable === true))
{
return FunctionBuilder.fromFunction(value);
}
return Type.store.get(
(typeof value === "function" && value.prototype?.[REFLECTED_TYPE_ID])
|| value.constructor.prototype[REFLECTED_TYPE_ID]
) || Type.Unknown;
}

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.