#Generic type that can be one of a set of custom types

58 messages · Page 1 of 1 (latest)

wheat spire
#

I'm trying to pass a type to a React hook:

export function useMyHook<T>() {
  const ref = useRef<T>(null!)
  
  useEffect(() => {
    ref.current.method() // TypeError: Method does not exist on type
  }, [])
  
  return ref
}

And use it like this:

// I want to be able to use multiple types here. MyType, ThatType, ThisType, etc..
const useMyHook<MyType>()

The problem is that the methods on each type I want pass are different, but do the same thing. I also don't want to have to include the ref in each component.

I assume I can do something like this and cast the type when I define the ref?

type RefType = 'MyType' | 'ThatType' | 'ThisType'
export function useMyHook(refType:RefType) {}

But not sure if this is the best way. Ideally I'd like to just decide which methods are available on the ref without TypeScript complaining.

void drum
#

I'm a bit confused. How do you expect JS to know which method to call if it can vary wildly between ref types

#

@wheat spire pinging because late answer

wheat spire
#

I should have been more specific. I plan on using types with methods that I know ahead of time. Restricting it to one of a group of specific types would be ideal.

For example,
ThisType has the method present() and
ThatType has the method show() and
OtherType has the method expand() and so on..

Inside the hook I plan on simply checking which method is available:

  useEffect(() => {
    if (ref.current.method) {
      ref.current.method() // TypeError: `method` does not exist on type
    }
  }, [])

However, inside the hook the type is still unknown and generic. So, TypeScript complains. The purpose of passing a type in the first place is so the type of the returned ref is correct for the component:

const ref = useMyHook<MyType>()
// Can use this on my component without it complaining
fluid oak
#

you could create a type that defines all of the functions as optional properties

void drum
#
  useEffect(() => {
    if (ref.current.method) {
      ref.current.method() // TypeError: `method` does not exist on type
    }
  }, [])

I would consider this to be an antipattern

#

It reduces everything to manual ducktyping

#

Either let the types you are passing in implement a common interface that your hook calls

#

Or, pass the proper way of accessing in an external way

#

E.g., by also passing a function that will call the proper method

#

Or, if you don't want to pass extra parameters and want the access method be associated to the type but for some reason not in the type, you could use some kind of mapping object.

#

Or, wrap the type in another object that forms a discriminated union

#

Or, expose the "way of accessing" as some kind of enumeration property on your type

#

Any of those methods will be more deterministic and extensible than checking if a method exists.

wheat spire
#

I'm not really following.

What if I simplified my example even further and say I only want to be able to use two types:
ThisType and ThatType contain the same methods with the exception of one containing present() and the other expand(). These two methods do the exact same thing.

It seems logical to extract this logic with a hook and return a ref with the proper type.

So:

const ref = useMyHook<ThisType>() // ref is of type ThisType
const ref = useMyHook<ThatType>() // ref is of type ThatType
// but need to access the methods on the ref within the hook?

I know I can define the ref within each component and pass as a parameter that can be RefObject<ThisType | ThatType>?

wheat spire
void drum
#

i’ll answer in 5 minutes, on the road

clear hinge
#

i'd cracked this open already, so sorry to butt in, but is this what you're trying to do?

halcyon crescentBOT
#
mkantor#0

Preview:```ts
import {useRef, useEffect} from "react"

type ThisType = {present: () => void}
type ThatType = {show: () => void}
type RefType = ThatType | ThisType

function useMyHook<T extends RefType>() {
const ref = useRef<T>(null!)

useEffect(() => {
if ("present" in ref.current)
...```

clear hinge
#

i don't use react so can't really say whether this is an anti-pattern. does give me weird vibes though

#

i would at least consider using a discriminated union, because there's nothing in this code preventing someone from using an object that has both show and present methods

#

and if you don't want to go with a discriminated union for some reason i'd instead consider @fluid oak's suggestion (#1180127987526537226 message) as it's simpler and makes it more obvious what is actually possible at runtime

wheat spire
#

Saying that is it one type or the other is fine with me. (I assume that's what you mean by discriminated union in your example)

#

Note that both of the types I'm trying to use are not my own

clear hinge
#

that's what a discriminated union represents, yeah, but there's a specific way to encode them in typescript. if you follow that link you can read about it

#

again i don't use react so this might be a senseless suggestion, but if all of the methods share the same signature then could you just make the ref capture that function itself rather than the object that contains it?

#

maybe something like:

function useMyHook() {
  const ref = useRef<() => void>(null!)
  
  useEffect(() => {
    ref.current()
  }, [])
  
  return ref
}

🤷

void drum
#

Not to derail the current discussion, but:
I was a bit quick in judging this as an “antipattern” because TS is cool exactly because it allows this level of flexibility, but what I meant was, you lose some reliability by not specifying the access pattern explicitly. E.g. suppose your checks first check for show and then for present after

#

now you implement type A with the method present

#

Someone else later comes in and doesn’t know about this whole mechanism and writes a class C extends A and implements show because they want to show something

#

now suddenly this object which should have been called present on gets called using show instead

#

and from the outside it’s not obvious that this will happen

wheat spire
#

So, then, is there a way to restrict it to specific types? The two mentioned for example.

clear hinge
#

you use extends to specify a constraint on the type parameter. did you see the playground i shared above?

clear hinge
#

i dunno if that null! exists in your real code or where the actual ref value comes from at the end of the day

wheat spire
#

I mean.. They do, but typescript doesn't know

clear hinge
#

not sure what you mean. could you share an example?

wheat spire
#

Well take your code:

#
function useMyHook() {
  const ref = useRef<() => void>(null!)
  
  useEffect(() => {
    ref.current()
  }, [])
  
  return ref
}

Plug it into a component:

const ref = useMyHook()
//...
<View ref={ref} />

Now the ref is technically RefObject<View>, but no way to recognize that inside the hook.

#

null! is just a way of telling TypeScript that .current is not undefined. Because at runtime it won't be, even though it is when it is defined.

#

A safer way would be using ref.current?.method()

clear hinge
#

i guess i don't know at all how react hooks work. forget about my latest suggestion, with your original version (or your ideal version) how would a user pass in a value of ThisType or ThatType? in what you just shared the ref value doesn't appear to ever get initialized

wheat spire
#

I don't know if I should really get into the magic of React, but this is how I would initialize with the type:

const ref = useMyHook<ThisType>() // The hook returns a ref:RefObject<ThisType>
//...
// The `ref` prop here expects a ref object of ThisType
<MyComponent ref={ref} />
clear hinge
#

sorry i'll rephrase the question: inside useEffect you're calling a method on a value. where did that value come from? somebody must have initialized a value of type ThisType or ThatType at some point in your program for this to make sense

#

(i'm only trying to grok this btw so i can communicate the suggestion i was trying to give you about passing the method itself rather than the object that has a method on it, but i don't know how to frame that if i don't know how the value gets in there to begin with)

wheat spire
#

The types come from a package that provide two types of modal components

clear hinge
#

so that package is creating a value of type ThisType or ThatType and giving it to your code somewhere along the line?

wheat spire
#

Well, technically they are components.

clear hinge
#

components are values too 😄

#

okay if you're saying what i think you're saying then my "just pass the function" suggestion doesn't make sense, because you aren't in control of how the ref's value is provided. i was suggesting you change that part

#

sorry for the long tangent

wheat spire
#

Your original suggestion on the playground seems reasonable

clear hinge
#

yeah, i think that's what you were originally looking for

wheat spire
#

Interestingly, when using

if ('expand' in ref.current) {
  ref.current.expand()
}

The method loses its type clarity on my IDE. Now it thinks it is any