Takeshi Tokugawa YD
Takeshi Tokugawa YD

Reputation: 960

TypeScript does not see that undefined\null value checking has been executed in variable

In below example, TypeScript does not see that parameters.a and parameters.b has been checked for undefined value and in transformValue(parameters.a) line could not be undefined:

type Example = {
    a?: string,
    b?: string
}

function example(parameters: {
    a?: string,
    b?: string,
    skipA?: boolean,
    skipB?: boolean
}): Example {

    const shouldReturnA = typeof parameters.a !== "undefined" && parameters.skipA !== false;
    const shouldReturnB = typeof parameters.b !== "undefined" && parameters.skipB !== false;

    return {
        ...shouldReturnA ? { a: transformValue(parameters.a) } : {},
        ...shouldReturnB ? { b: transformValue(parameters.b) } : {}
    }
}

function transformValue(targetValue: string): string {
    return targetValue + "~~~";
}
Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.(2345)

Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.(2345)

🌎 Fiddle

Well, I can rewrite this synthetic example like this:

function example(parameters: {
    a?: string,
    b?: string,
    skipA?: boolean,
    skipB?: boolean
}): Example {
  return {
      ...typeof parameters.a !== "undefined" && parameters.skipA !== false ? { 
        a: transformValue(parameters.a) 
      } : {},
      ...shouldReturnB = typeof parameters.b !== "undefined" && parameters.skipB !== false ? { 
        b: transformValue(parameters.b) 
      } : {}
  }
}

But how about below live example?

public static normalizeRawConfig(
  {
    pickedFromConsoleInputConfig,
    rawValidConfigFromFile
  }: {
    pickedFromConsoleInputConfig: ProjectBuilderRawConfigNormalizer.PickedFromConsoleInputConfig;
    rawValidConfigFromFile: ProjectBuilderRawValidConfigFromFile;
  }
): ProjectBuilderNormalizedConfig {

  const markupPreprocessingConfigNormalizingIsRequired: boolean =
      !isUndefined(rawValidConfigFromFile[ProjectBuilderTasksIDsForConfigFile.markupPreprocessing]) &&
      (
          isUndefined(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection) ||
          Object.keys(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection)
              .includes(ProjectBuilderTasksIDsForConfigFile.markupPreprocessing)
      );

  const stylesPreprocessingConfigNormalizingIsRequired: boolean =
      !isUndefined(rawValidConfigFromFile[ProjectBuilderTasksIDsForConfigFile.stylesPreprocessing]) &&
      (
          isUndefined(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection) ||
          Object.keys(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection)
              .includes(ProjectBuilderTasksIDsForConfigFile.stylesPreprocessing)
      );

  return {
    ...markupPreprocessingConfigNormalizingIsRequired ? {
      markupPreprocessing: MarkupPreprocessingRawSettingsNormalizer.getNormalizedSettings(
          rawValidConfigFromFile.markupPreprocessing, commonSettings__normalized
      )
    } : {},
    ...stylesPreprocessingConfigNormalizingIsRequired ? {} : {
      stylesPreprocessing: StylesPreprocessingRawSettingsNormalizer.getNormalizedSettings(
          rawValidConfigFromFile.stylesPreprocessing, commonSettings__normalized
      )
    }
  };
}

If rewrite it without markupPreprocessingConfigNormalizingIsRequired and stylesPreprocessingConfigNormalizingIsRequired, it will become to:

return {
  ...!isUndefined(rawValidConfigFromFile[ProjectBuilderTasksIDsForConfigFile.markupPreprocessing]) &&
    (
        isUndefined(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection) ||
        Object.keys(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection)
            .includes(ProjectBuilderTasksIDsForConfigFile.markupPreprocessing)
    ) ? {
    markupPreprocessing: MarkupPreprocessingRawSettingsNormalizer.getNormalizedSettings(
        rawValidConfigFromFile.markupPreprocessing, commonSettings__normalized
    )
  } : {},
  ...!isUndefined(rawValidConfigFromFile[ProjectBuilderTasksIDsForConfigFile.stylesPreprocessing]) &&
    (
        isUndefined(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection) ||
        Object.keys(pickedFromConsoleInputConfig.tasksAndSourceFilesSelection)
            .includes(ProjectBuilderTasksIDsForConfigFile.stylesPreprocessing)
    ) ? {
    stylesPreprocessing: StylesPreprocessingRawSettingsNormalizer.getNormalizedSettings(
        rawValidConfigFromFile.stylesPreprocessing, commonSettings__normalized
    )
  } : {}
};

And also, the conditions will become complicated in the future.

Some best practices to dealing with it in TypeScirpt?

Update

Tried to comparing XXX !== undefined instead of typeof XXX !== "undefined. Same effect.

enter image description here

Upvotes: 0

Views: 1241

Answers (2)

Henry Mueller
Henry Mueller

Reputation: 1327

Why doesn't it recognize undefined checking was done?

The underlying issue that causes the TS compiler to not realize a or b have been checked has to do with how "type guards" work. You have a type guard in your code parameters.a !== undefined (the check on skipX has nothing to do with type checking, so leaving that out of example). If you were to use that like this:

if (parameters.a !== undefined) {
  transformValue(parameters.a)
}

TS would be perfectly happy with that, because the transformValue call is inside the if statement. That is how type guards work, TS uses them to "narrow" the type of the variable with in the if block. In your case, TS is not setup to understand that you previously narrowed the type when you set the value for shouldReturnA--humans can see the logic of it, but to TS that is ancient history which it has forgotten about by the time it gets to your ...shouldReturnA ? code.

Alternate approach

This is not as quick a fix as @dongnhan's suggestion to use ! after the variable, but maybe you will like it. I think it organizes the logic a bit better and makes it a bit more DRY, but that is my opinion.

type Example = {
    a?: string,
    b?: string
}

function example(parameters: {
    a?: string,
    b?: string,
    skipA?: boolean,
    skipB?: boolean
}): Example {

    const validatedProp = (prop: string | undefined, shouldSkip: boolean | undefined) => 
      !shouldSkip && typeof prop === "string" ? { a: transformValue(prop) } : {}

    return {
        ...validatedProp(parameters.a, parameters.skipA),
        ...validatedProp(parameters.b, parameters.skipB),
    }
}

function transformValue(targetValue: string): string {
    return targetValue + "~~~";
}

TS Playground Still room for much more improvement with further refactoring.

My Opinion About typeof undefined vs 'undefined'

Please listen to ESLint on this one. The typeof keyword will return a string "undefined", not the value undefined. Your type guard will not work unless you use the string (although note I compare for the positive match to "string")

Upvotes: 4

dongnhan
dongnhan

Reputation: 1768

Based on the Typescript handbook, there is something called non-null assertion operator:

A new ! post-fix expression operator may be used to assert that its operand is non-null and non-undefined in contexts where the type checker is unable to conclude that fact. Specifically, the operation x! produces a value of the type of x with null and undefined excluded. Similar to type assertions of the forms <T>x and x as T, the ! non-null assertion operator is simply removed in the emitted JavaScript code.

type Example = {
    a?: string,
    b?: string
}

function example(parameters: {
    a?: string,
    b?: string,
    skipA?: boolean,
    skipB?: boolean
}): Example {

    const shouldReturnA = parameters.a !== undefined && parameters.skipA !== false;
    const shouldReturnB = parameters.b !== undefined && parameters.skipB !== false;

    return {
        ...shouldReturnA ? { a: transformValue(parameters.a!) } : {},
        ...shouldReturnB ? { b: transformValue(parameters.b as string) } : {} // another way
    }
}

function transformValue(targetValue: string): string {
    return targetValue + "~~~";
}

Here's one helpful article about non-null assertion operator: https://medium.com/better-programming/cleaner-typescript-with-the-non-null-assertion-operator-300789388376

Upvotes: 4

Related Questions