typescripttypescript-genericstypescript-conditional-types

Switch return types based on parameter object's property


I'd like to switch the return type of my function depending on whether the type of a property on an object passed in is a string or undefined.

What I've tried is available on the TypeScript playground here, but I'll paste a copy below:

type WithLocale = {
    en: string
    fr: string
    de: string
}

type WithoutLocale = string

type GetItemParams = {
    locale?: string | undefined
}

function getItem(options: GetItemParams): typeof options['locale'] extends string ? WithoutLocale : typeof options['locale'] extends undefined ? WithLocale : never {
    if ('locale' in options && typeof options.locale === 'string') {
        return 'test string'
    } else {
        return {
            en: 'test string en',
            fr: 'test string fr',
            de: 'test string de',
        }
    }

}

The compiler always thinks that the return type is never. I've tried swapping to a generic with no joy:

type WithLocale = {
    en: string
    fr: string
    de: string
}

type WithoutLocale = string

type GetItemParams<T> = {
    locale?: T
}

type GetItemReturnType<T> = T extends string ? WithoutLocale : T extends undefined ? WithLocale : never

function getItem<T extends string | undefined = undefined>(options: GetItemParams<T>): GetItemReturnType<T> {
    if ('locale' in options && typeof options.locale === 'string') {
        return 'test string'
    } else {
        return {
            en: 'test string en',
            fr: 'test string fr',
            de: 'test string de',
        }
    }

}

I can get this working if I manually add a boolean property to identify whether I want all locales or not, but this isn't what I'm after.

Any assistance much appreciated.


Solution

  • I like to use function overloads for situations where a function can return various return types based on some criteria.

    You can declare one overload with a Required parameter which is going to return WithLocal since GetItemParams["locale"] is definitely not going to be undefined.

    type WithLocale = {
      en: string;
      fr: string;
      de: string;
    };
    
    type WithoutLocale = string;
    
    type GetItemParams = {
      locale?: string | undefined;
    };
    
    function getItem(options: Required<GetItemParams>): WithLocale;
    function getItem(options: GetItemParams): WithoutLocale;
    function getItem(options: GetItemParams): WithLocale | WithoutLocale {
      if ("locale" in options && typeof options.locale === "string") {
        return "test string";
      } else {
        return {
          en: "test string en",
          fr: "test string fr",
          de: "test string de",
        };
      }
    }
    
    const withLocal = getItem({locale: "en"});
    //    ^? const withLocal: WithLocale
    const withoutLocal = getItem({});
    //    ^? const withoutLocal: string
    

    TypeScript Playground


    Caveat; You have to be careful with how you call the function since this approach is purely based on the type of the input argument! In the following example locale is set yet due to the explicit type annotation its type is string | undefined.

    const local: GetItemParams = { locale: "en" };
    const incorrectWithoutLocal = getItem(local);
    //    ^? const incorrectWithoutLocal: string
    

    Use satisfies to prevent this.

    const local = { locale: "en" } satisfies GetItemParams;
    const correctWithLocal = getItem(local);
    //    ^? const correctWithLocal: WithLocale