Reputation: 960
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)
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?
Tried to comparing XXX !== undefined
instead of typeof XXX !== "undefined
. Same effect.
Upvotes: 0
Views: 1241
Reputation: 1327
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.
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.
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
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
andx 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