#Best way to check if a type is an object

41 messages · Page 1 of 1 (latest)

trail sand
#

In my code, I did something quite long — I check if it's a function, an array, a primitive, etc., and if not, and it extends from object, I say it's an object. But I'm not sure if that's the right way to do it

Naive solution :

type t_notObject = string|undefined|null|readonly any[]|number
type t_isObject<T> = T extends t_notObject ? false : true  //TODO
drifting vine
#

you could just do extends object and then check for a call/construct signature or array

#

(fyi your naive solution would also say symbols, booleans, bigints, functions, and classes are objects)

trail sand
#

like this : typescript type Constructed<T> = new () => T; type t_objectConstructor = ObjectConstructor | ProxyConstructor | MapConstructor | WeakMapConstructor |SetConstructor | WeakSetConstructor type t_isObject<T> = T extends object ? T extends Constructed<infer C> ? C extends t_objectConstructor ? true : false : [keyof T] extends [never] ? {} extends T ? true : false : true :false

#

Thanks for your quick answer btw

drifting vine
#

that doesn't seem to really make sense

#

you're asking if C, an instance, extends t_objectConstructor, which are all classes?

trail sand
#

💀

drifting vine
#

if you're looking for a non-function, non-class, non-array, object, then it's kinda just that

#
type IsObject<T> =
  T extends object
    ? T extends
      | ((...args: never) => unknown)
      | (abstract new (...args: never) => object)
      | readonly unknown[]
      ? false
      : true
    : false
tribal ginkgo
#

hax:

blazing harnessBOT
#
type IsObject<T> =
  T extends object
    ? T extends
      | ((...args: never) => unknown)
      | (abstract new (...args: never) => object)
      | readonly unknown[]
      ? false
      : true
    : false

const x: {} = ''
type X = IsObject<typeof x>
//   ^? - type X = true
tribal ginkgo
#

the reality is that there's no way to do this that's completely robust. maybe that doesn't matter for your use case, but IMO in TS it's usually better to think in terms of what properties values have, rather than what "kind" of value it is

#

@trail sand why do you want to do this in the first place?

drifting vine
#

the real issue is that js doesn't really expose true primitives

#

aside from null and undefined, everything can be used as if it were an object

trail sand
drifting vine
#

you don't need to

#

you'd just check if it was a constructor or not

tribal ginkgo
#

(i think they meant they didn't know how to write the construct signature type)

trail sand
# tribal ginkgo <@861677933810941973> why do you want to do this in the first place?

I’ve always done it by exclusion — for example, it’s an object, but it’s neither an array, nor a function, nor undefined or null, so it must be a proper object as I understand it. But it never felt natural to me.
By the way, do you know a good resource to learn how to work with classes — like extracting the constructor, checking if a class is abstract, and so on ?
Thanks again for your help, that_guy and mkantor !

#

!resolve

drifting vine
#

a class is the constructor

#

an abstract class is one where the constructor is abstract

#
type IsAbstract<T extends abstract new (...args: never) => object> = T extends new (...args: never) => object ? false : true
#

though the opposite would probably be more useful

#
type IsConcrete<T extends abstract new (...args: never) => object> = T extends new (...args: never) => object ? true : false
trail sand
# drifting vine a class *is* the constructor

Thinking about it that way makes so much sense, yes. I remember some weird stuff, like having to use typeof on the class to extract static functions, but I’m not entirely sure anymore. I know there’s a lot I’ve forgotten — and even more I never really understood or mastered when it comes to all those dynamics.

drifting vine
#

ah yeah ok, this is kinda an unintuitive line

#

in value space, a class is the constructor, and static fields and methods of the class are on the constructor

#

in type space, using the class name refers to its interface, aka an instance, aka the non-static stuff
so when you want to refer to the constructor of a specific class in type-space, you have to grab the value-space version (the constructor) and then get the type of that, hence typeof Class

trail sand
#

I think I understand what you're saying — it's really interesting, and no one has ever really explained it to me like that. Over time, I noticed the behavior was different just by experimenting. Where did you learn all this? Do you have any resources you’d recommend? I’m really interested in TypeScript, but I’ve had a hard time finding truly relevant content on it

Thanks again for your explanations, they help a lot. I'll have to read your message several times to really let it sink in, but thank you!!!

drifting vine
#

this kind of thing i typically learn through understanding the underlying systems, so... honestly probably not something i'd recommend to a beginner 😓

#

the only resource i can personally recommend is the handbook, but that's not to say there aren't other good resources; i'm just not familiar with others lol

#

!hb

blazing harnessBOT
tribal ginkgo
#

another way to put it is that when you write class Foo { … } you're defining both a type (the type that instances will conform to) and a value (the thing you new, same as what the name Foo refers to at runtime)

conceptually, it's as if this:

class Foo { … }

is syntax sugar for this:

interface Foo { … }
const Foo = class { … }
#

the same sort of thing happens with the enum keyword: that one statement defines both a type (the type that is a union of all the enum members' types) and a value (an object containing a property for each enum member)

tribal ginkgo
tribal ginkgo
#

(feel free to ignore me if you're satisfied with the help you got already, but IMO needing this sort of type is a possible indicator of deeper design issues)