Jump to content
Search Community

Declare method types (with typescript) on GSAP context object

gabriel.ortiz test
Moderator Tag

Go to solution Solved by gabriel.ortiz,

Recommended Posts

Hi there!

 

I'm really in love with the new GSAP Context! It's really cool with working with React!

 

When i add timelines with `context.add()` I get a typescript error because the method does not exist as a property of the context object.

 

So the question is: How do I declare a type in typescript (and GSAP) that I intend to add a specific timeline method to the GSAP context? 

 

Here's an example:

// Borrowing this context hook from the docs @see: https://greensock.com/react-advanced#useGsapContext
export function useGsapContext<T extends HTMLElement = HTMLDivElement>(
  scope: RefObject<T>,
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  context: gsap.ContextFunc = () => {},
) {
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const ctx = useMemo(() => gsap.context(context, scope), [scope]);
  return ctx;
}

 

I see that the return types for context are:

  interface Context {
    [key: string]: any;
    selector?: Function;
    isReverted: boolean;
    conditions?: Conditions;
    queries?: object;
    add(methodName: string, func: Function, scope?: Element | string | object): Function;
    add(func: Function, scope?: Element | string | object): void;
    ignore(func: Function): void;
    kill(revert?: boolean): void;
    revert(config?: object): void;
    clear(): void;
  }

 

Now I'm adding a method to the context like so:
 

// Init the gsap context
const ctx = useGsapContext(wrapperRef);


// Adds the timeline method to the context. This useEffect runs only once after initial render
useEffectOnce(() => {
  ctx.add("newTab", (newIdentifier: string, oldIdentifier: string) => {

    const { current: wrapperEl } = wrapperRef;

    if (!wrapperEl) return;

    const tl = gsap.timeline();

    if (oldIdentifier) {
      tl.to(`[data-tab="${oldIdentifier}"]`, {
        duration: 0.5,
        scale: 0.9,
        autoAlpha: 0,
      });
    }

    tl.fromTo(
      `[data-tab="${newIdentifier}"]`,
      {
        scale: 1.2,
        autoAlpha: 0,
      },
      {
        scale: 1,
        autoAlpha: 1,
      },
    );
  });
});


// on state update - uses the timeline we added to the context
useUpdateEffect(() => {

  // Using the method added to context (this works!) But typescript complains this method doesn't exist
  if ("newTab" in ctx && typeof ctx["newTab"] === "function") {
    ctx.newTab(activeValue.active, activeValue.prev);
  }
}, [activeValue]);

 

So the timeline works as expected which is great... but I get the following error which is expected because typescript doesn't know there is a "newTab" property on the context object

image.png.c1fe3497620586c85be2279502b84694.png

 

Link to comment
Share on other sites

3 hours ago, gabriel.ortiz said:

I'm really in love with the new GSAP Context! It's really cool with working with React!

Love hearing that. 🥳

 

3 hours ago, gabriel.ortiz said:

How do I tell typescript (and GSAP) that I intend to add a specific timeline method to the GSAP context? 

I'm totally not a TypeScript expert but I'm pretty sure this is all related to TypeScript and/or linting settings. Maybe someone else has an idea for you, but as far as I can tell it's not technically invalid but perhaps you've got to change a setting somewhere on your end?: 

https://typescript-eslint.io/rules/no-unsafe-call/

 

I wish I was more of a TypeScript expert and could tell you exactly what to do. 🤷‍♂️

Link to comment
Share on other sites

  • gabriel.ortiz changed the title to Declare method types (with typescript) on GSAP context object
On 1/6/2023 at 9:16 PM, GreenSock said:

Love hearing that. 🥳

 

I'm totally not a TypeScript expert but I'm pretty sure this is all related to TypeScript and/or linting settings. Maybe someone else has an idea for you, but as far as I can tell it's not technically invalid but perhaps you've got to change a setting somewhere on your end?: 

https://typescript-eslint.io/rules/no-unsafe-call/

 

I wish I was more of a TypeScript expert and could tell you exactly what to do. 🤷‍♂️

 

I'm no Typescript rockstar either... I know enough to get myself in trouble though haha!

 

Basically I think it would be beneficial if we could pass in an optional object type declarations to the gsap.context() method, telling the context typescript method that it can expect a custom property.  Something like this.. (and this is very rough and unpolished)

 

type GSAPHandlers = {
  newTab: (activeTab:string, oldTab:string)=>void;
  otherMethods: (stuff:boolean)=>void;
  ...
};

// Pass in expected types
// The generic passed in would likely need to be converted to a Partial<>, or else Typescript would complain if the additional method is not present when the function executes.
const ctx = gsap.context<GSAPHandlers>(() => {
});

// Add the method to the context
ctx.add("newTab", (activeTab:string, oldTab:string)=>{...});
                                                      
                                                      
// Use the method!!
ctx.newTab('foo', 'bar');                                                      
                                                      
  

 

@GreenSock are there any other ways to add methods to GSAP context? Context is incredibly helpful because it also allows for concise organization of greensock timelines. I know there are other ways of organizing timelines into methods like this - but the x-factor with context is the access to the revert() method which is brilliant!

Link to comment
Share on other sites

2 hours ago, gabriel.ortiz said:

@GreenSock are there any other ways to add methods to GSAP context? Context is incredibly helpful because it also allows for concise organization of greensock timelines. I know there are other ways of organizing timelines into methods like this - but the x-factor with context is the access to the revert() method which is brilliant!

Well keep in mind that all animations have a .revert() method too, so if you're asking if you could put everything into one timeline and then call revert() on that, the answer is yes :)

 

If you're asking me how to add TypeScript definitions for your own custom methods on a Context instance, I'm just not familiar enough with TypeScript to give you an answer on that but I'd guess that it must be entirely doable. It'd be weird if you simply couldn't set up your own custom definitions like that. Maybe ask in a more TypeScript-focused community? I sure wish I could offer more TypeScript assistance. Maybe someone else here will chime in with an answer. 

  • Like 1
Link to comment
Share on other sites

  • Solution

Thanks so much @GreenSock! I appreciate your response!

 

So I got something working for me in case others might encounter the same situation with context. I ended up modifying the react Gsap context hook to cast the return context types with an optional object that you can pass in through a generic

 

/**
 * Wrapper hook initializes GSAP context object
 *
 * @param {RefObject}
 * @param {gsap.ContextFunc}
 * @returns {gsap.Context}
 */
export function useGsapContext<
  T extends HTMLElement = HTMLDivElement,
  R extends Record<string, unknown> = Record<string, unknown>,
  // It's ok to allow an empty function since gsap.context() needs a function argument to initialize  
  // eslint-disable-next-line @typescript-eslint/no-empty-function
>(scope?: RefObject<T>, context: gsap.ContextFunc = () => {}) {
  return useMemo(
    () => gsap.context(context, scope),
    // It's not necessary to list `context` as a dependency
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [scope],
    /**
     * Normally its hacky to type assert - but since this context logic is
     * isolated in this hook there is little risk for these types causing issues
     * with other implementations of GSAP context
     */
  ) as Partial<R> & gsap.Context;
}

/**
 * These types will be added to the return of the gsap context so typescript
 * will know to expect these properties if referenced later
 */
type ContextMembers = {
  newTab: (newTabKey: string, oldTabKey: string | null) => void;
};

const wrapperRef = useRef<HTMLDivElement>(null);
const ctx = useGsapContext<HTMLDivElement, ContextMembers>(wrapperRef);

useEffect(()=>{
	ctx.add("newTab", (newTabKey: string, oldTabKey: string | null) => {....})
}, []);

useEffect(()=>{
	ctx.newTab("foo", "bar");
},[tabState]);

 

 

Additionally, I noticed gsap.context() will return undefined if no function argument is passed in. Is this expected? The Context types have the callback function as optional argument, and but it appears to be required type argument or else nothing is initialized.

// gsap-core.d.ts   
function context(func?: ContextFunc, scope?: Element | string | object): Context;

Correct me if i'm wrong - but it could possibly be defined like this:

function context(func: ContextFunc, scope?: Element | string | object): Context;

 

  • Like 1
Link to comment
Share on other sites

3 hours ago, gabriel.ortiz said:

Additionally, I noticed gsap.context() will return undefined if no function argument is passed in. Is this expected? The Context types have the callback function as optional argument, and but it appears to be required type argument or else nothing is initialized.

 

That's very intentional - gsap.context() can also serve as a getter but that's mostly for internal use. In other words, gsap.context() (when you don't pass in any function) will return the currently-processing Context (if there is one). So in your case, you must have called gsap.context() from outside a context function execution, thus it returned undefined (correctly - there was no Context currently being executed). See what I mean? 

 

Glad you figured out a solution - thanks for sharing! 🙌

  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...