Reproduction Link

lucid-jasper-8kyn6t - CodeSandbox

Question

  1. Depending on the optionality of a specific type, I want to infer a different return type for functions and class methods. Is there a way to achieve this?
  2. In this process, is there a way to achieve the goal without using the "as" (type assertion)?
/* Definition Part */
import { parseOptionalNfValue } from "./parse-nf-value";
import { parseNumberWithSuffix } from "./parse-number-with-suffix";

export type ParseNfValue = string | number;

export type ParamOfParseNumberWithSuffix = Parameters<
  typeof parseNumberWithSuffix
>;
export type NfOptions = ParamOfParseNumberWithSuffix[1];

type Optional<T> = T | undefined;

export class Nf<T extends number | string | undefined | null> {
  number: T extends null | undefined ? Optional<number> : number;
  currency: T extends null | undefined ? Optional<string> : string;
  currencyWithWon: T extends null | undefined ? Optional<string> : string;
  nf: NfOptions;

  constructor(nf: NfOptions, nfValue?: T) {
    this.nf = nf;
    // I use as 
    this.number = parseOptionalNfValue(nf, nfValue) as T extends
      | null
      | undefined
      ? Optional<number>
      : number;

    this.currency = parseOptionalNfValue(nf, nfValue) as T extends
      | null
      | undefined
      ? Optional<string>
      : string;
   
    this.currencyWithWon = parseOptionalNfValue(nf, nfValue) as T extends
      | null
      | undefined
      ? Optional<string>
      : string;
  }
}

Goal

Intention: I want the return type of the nf class method to vary based on the optionality of the value. like below

/* Usage Part */

/* type test*/
const optionalNf1: number | undefined = undefined;
export const test1Nf = new Nf('0f', optionalNf1);

test1Nf.currency; // test1Nf should be inferred as a type of number | undefined.

const currencyWithSuffix1 = test1Nf.toCurrencyWithSuffix('원');

currencyWithSuffix1; // currencyWithSuffix1 should be inferred as a type of string | undefined.

const optionalNf2 = 1;
export const test2Nf = new Nf('0f', optionalNf2);
test2Nf.currency; // test2Nf should be inferred as a type of string

const currencyWithSuffix2 = test2Nf.toCurrencyWithSuffix('원');

/* #issue 
* currencyWithSuffix2 should be inferred as a type of string
* but currencyWithSuffix2 is inferred as string | undefined
* I want the return type to be inferred as string.
*/
currencyWithSuffix2;

/* Definition Part */
import { parseOptionalNfValue } from "./parse-nf-value";
import { parseNumberWithSuffix } from "./parse-number-with-suffix";

export type ParseNfValue = string | number;

export type ParamOfParseNumberWithSuffix = Parameters<
  typeof parseNumberWithSuffix
>;
export type NfOptions = ParamOfParseNumberWithSuffix[1];

type Optional<T> = T | undefined;

export class Nf<T extends number | string | undefined | null> {
  number: T extends null | undefined ? Optional<number> : number;
  currency: T extends null | undefined ? Optional<string> : string;
  currencyWithWon: T extends null | undefined ? Optional<string> : string;
  nf: NfOptions;

  constructor(nf: NfOptions, nfValue?: T) {
    this.nf = nf;

    this.number = parseOptionalNfValue(nf, nfValue) as T extends
      | null
      | undefined
      ? Optional<number>
      : number;

    this.currency = parseOptionalNfValue(nf, nfValue) as T extends
      | null
      | undefined
      ? Optional<string>
      : string;

    this.currencyWithWon = parseOptionalNfValue(nf, nfValue) as T extends
      | null
      | undefined
      ? Optional<string>
      : string;
  }
  // TODO: implement toNfString
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  toNfString(nf?: NfOptions): string {
    return this.nf;
  }
  toCurrencyWithSuffix(suffix: string): string | undefined {
    if (this.currency == null) {
      return;
    }
    return `${this.currency}${suffix}`;
  }
}

background

Our team's server use utilizes grpc and protobuf. Due to internal history, we can't use grpc directly on the web client and instead proxy grpc through REST. Given this background, all number types provided by the internal server API are delivered in the form of a number string, like {"amount0f: "10000"}

In the web client, we use the nf format of number strings in various ways.

To facilitate such flexible usage, I am planning to create an 'nf' class and introduce methods that return commonly used values. However, there's an issue when dealing with optional values. To reduce unnecessary if statements in usage, when nf is not optional, I want it to infer a fixed return type T, and when it's optional, we aim for it to infer as T | undefined.

To solve the aforementioned issue, I tried following

To achieve the above goal, by utilizing function overloading, I successfully implemented the function as described below.

import { parseNumberWithSuffix } from 'parse-number-with-suffix';
import { isNil } from 'remeda';

export function parseOptionalNfValue<T extends number | string>(nf: string, nfValue: T): number;
export function parseOptionalNfValue(
  nf: string,
  nfValue: undefined | null | string | number
): number | undefined;
export function parseOptionalNfValue(
  nf: string,
  nfValue?: number | string | undefined | null
): number | undefined {
  if (isNil(nfValue)) {
    return undefined;
  } else {
    return parseNumberWithSuffix(String(nfValue), nf);
  }
}

/* type test*/
const optionalNf1: number | undefined = undefined;
export const test1 = parseOptionalNfValue('0f', optionalNf1);
test1; // test1 should be inferred as a type of number | undefined.

const optionalNf2 = 1;
export const test2 = parseOptionalNfValue('0f', optionalNf2);
test2; // test2 should be inferred as a type of number