0

I'm not sure why Typescript gives a type error for the following code:

type ShareData = {
  title?: string;
  text?: string;
  url?: string;
};

// extending window.navigator, which has a type of Navigator
interface Navigator {
  share?: (data?: ShareData) => Promise<void>;
}

const isShareable =
    window.navigator && window.navigator.share;

  const handleShare = async () => {
    if (isShareable) {
      try {
        await window.navigator.share({
          text: 'Checkout Verishop!',
          title: 'Share Verishop',
          url: 'referralUrl',
        });
      } catch (e) {
        window.alert(e);
      }
    }
  };

Typescript complains about this line await window.navigator.share({

It says that share may be undefined. However, I am using isShareable to ensure that window.navigator.share is not undefined.

I also tried defining isShareable like the following: const isShareable = window.navigator && typeof window.navigator.share !== 'undefined'; but I would get the same error.

Ideally, I could create a function that would check if window.navigator.share is defined and that type-check would flow through.

All of the above code can be pasted into https://www.typescriptlang.org/play

Thanks!

3 Answers 3

1

Update for TS4.4:

TypeScript will soon allow control flow analysis of aliased conditions, meaning that saving a conditional like isShareable as a const may allow it to be used later as a type guard. Unfortunately the code here seems to have deteriorated a bit, since share is a known property of Navigator and you can't just merge it in anymore.

Aliased conditions like isShareable will only work if the properties you're testing are readonly, since for all the compiler knows, someone might set window.navigator.share = undefined at some point after you set isShareable but before you call window.navigator.share. This is quite a stumbling block if you're not in control of whether or not things are readonly. Still, if it looked like this:

interface Navigator extends Omit<typeof window.navigator, "share"> {
  readonly share?: (data?: ShareData) => Promise<void>;
}

declare const _window: Omit<typeof window, 'navigator'> & 
  { readonly navigator?: Navigator }

Then in TypeScript 4.4, this will not be an error:

  const isShareable =
    (_window.navigator && _window.navigator.share);

  const handleShareStored = async () => {
    if (isShareable) {
      try {
        await _window.navigator.share({ // no error in TS4.4+
          text: "Checkout Verishop!",
          title: "Share Verishop",
          url: "referralUrl"
        });
      } catch (e) {
        _window.alert(e);
      }
    }
  };

Playground link to code


Pre-TS4.4 answer:

TypeScript doesn't currently let you save type guard results in a variable to be used later the way you're trying to do with isShareable. There are two open suggestions asking for this in TypeScript's GitHub issues: microsoft/TypeScript#12184 and microsoft/TypeScript#24865. Neither of these issues seem to have much traction, but I suppose you can go to those issues and give them a 👍 or describe your use case if it's compelling. The problem is likely to be that it's not a trivial thing to implement and still maintain compiler performance.

What can you do right now? The obvious and unsatisfying answer is not to try to do your type guarding ahead of time, but do it right before you want to use the guarded window.navigator.share function:

// just do the check inline
const handleShareExplicit = async () => {
  if (window.navigator && window.navigator.share) { 
    try {
      await window.navigator.share({ // no error
        text: "Checkout Verishop!",
        title: "Share Verishop",
        url: "referralUrl"
      });
    } catch (e) {
      window.alert(e);
    }
  }
};

That's not really all that bad because it's still fairly terse... (window.navigator && window.navigator.share) still fits on one line. But some people might have a similar issue where the type guard is a much more elaborate check, or you might be philosophically opposed to doing your check more than once. In that case, you could try a different approach:

// store a reference to the function if it exists
const share =
  window.navigator && window.navigator.share
    ? window.navigator.share // .bind(window.navigator) ?? see note below
    : undefined;

const handleShare = async () => {
  if (share) {
    try {
      await share({ // no error
        text: "Checkout Verishop!",
        title: "Share Verishop",
        url: "referralUrl"
      });
    } catch (e) {
      window.alert(e);
    }
  }
};

Instead of storing a boolean-like variable isShareable, store the equivalent variable share whose type is ((data?: ShareData) => Promise<void>) | undefined. Then the check if (share) { share(...) } has the same semantics as what you were doing, except that in this case the compiler can easily see that you are using the thing you just checked.

(Do note that depending on how window.navigator.share() is implemented, it may need the this context to be window.navigator. If so, then you might want save share as window.navigator.share.bind(window.navigator) instead of just window.navigator.share. It probably doesn't hurt to do that in any case. It's up to you.)

Okay, hope that helps. Good luck!

Link to code

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for your detailed response. I actually had tried storing the share variable, but as you mentioned it needed access to this. I didn't think of binding it to window.navigator though. That may be the best solution for me.
0

You can use type casting to make sure that the navigator has the share method, and you need to make share a required prop because the way you have it, it COULD be undefined. No errors on this code in the playpen.

type ShareData = {
  title?: string;
  text?: string;
  url?: string;
};

// extending window.navigator, which has a type of Navigator
interface Navigator {
  share: (data?: ShareData) => Promise<void>;
}

const nav = window.navigator as Navigator;

  const handleShare = async () => {
      try {
        await nav.share({
          text: 'Checkout Verishop!',
          title: 'Share Verishop',
          url: 'referralUrl',
        });
      } catch (e) {
        window.alert(e);
      }
    }
  };

Comments

0

You can use a type guard to help TypeScript safely determine the interface of a variable. For example,

type ShareData = {
  title?: string;
  text?: string;
  url?: string;
};

// extending window.navigator, which has a type of Navigator
interface SharableNavigator extends Navigator {
  share: (data?: ShareData) => Promise<void>;
}

function isSharableNavigator(nav: Navigator): nav is SharableNavigator {
    return typeof (nav as SharableNavigator).share === 'function';
}

const handleShare = async () => {
  if (isSharableNavigator(window.navigator)) {
    try {
      await window.navigator.share({
        text: 'Checkout Verishop!',
        title: 'Share Verishop',
        url: 'referralUrl',
      });
    } catch (e) {
      window.alert(e);
    }
  }
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.