Using `enum` in ternary operator TypeScript - javascript

I am trying to set an object with a ternary operator based on a config value and an enum
import { config } from 'src/config'
import {logLevelEnum} from 'a-package-installed'
const someObject = {
logLevel: config.logLevel ? logLevelEnum[config.logLevel] : logLevelEnum.NOTHING,
}
The enum is basically this:
export enum logLevelEnum {
NOTHING = 0,
ERROR = 1,
WARN = 2,
INFO = 4,
DEBUG = 5,
}
But I get the compilation error:
Element implicitly has an 'any' type because index expression is not of type 'number'.
logLevel: config.logLevel ? logLevelEnum[config.logLevel] : logLevelEnum.NOTHING,
~~~~~~~~~~~~~~~
But I don't understand why it says that the index expression is supposed to be number since is an enum.
Can someone explain to me why and how can I achieve what I need?
Much appreciated.

The problem is that config.logLevel is of type string while there is actually only a subset of valid strings.
So declare config.logLevel as a union type: 'NOTHING' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'
This union type doesn't seem to be generatable from the enum according to this: Generic type to get enum keys as union string in typescript?

Related Typescript Playground Example
Some basics
You can access your enum in different ways:
logLevelEnum.WARN = 2: by the enum directly
logLevelEnum['WARN'] = 2: via index operator
logLevelEnum[2] = WARN: get the enum-name from an enum-value
This works because typescript creates a reverse mapping
When you try to access an invalid enum, you get undefined at runtime. Typescript tries to avoid this situation and gives you a compile error when possible to avoid this:
logLevelEnum.warning: Property 'warning' does not exist on type 'typeof logLevelEnum'.
logLevelEnum['warning'] = undefined: Element implicitly has an 'any' type because index expression is not of type 'number'.
this is maybe a little confusing, as it seems to indicate that you can
only use number as index - which is not true - see 2. above
but the basic statement is right: typescript cannot guarantee that this expression returns a valid enum, thus the type of this expression is any (and not logLevelEnum or logLevelEnum | undefined, etc.)
Hint: you can see the type when you hover over the invalidStringIndex variable in the Typescript Playground Example
logLevelEnum[999] = undefined:
unfortunately we don't get a compile error here, but it is obviously not a valid index
the type of this expression is string! But actually it can also be undefined.
Hint: when you activate the typescript-compiler option noUncheckedIndexedAccess, then the type will be string|undefined which is more accurate
Answer to your question
As I understand your question
config.logLevel is of type string and not under your control.
If it were under your control, the type should be logLevelEnum and everything would be easier: logLevelEnum[logLevelEnum] is then guaranteed to be a valid enum (at compile time)
you want to get a valid log-level: when config.logLevel is valid, you want to use it, otherwise you want to use logLevelEnum.NOTHING
So basically you need a function like this (which you call with config.logLevel):
function getValidLogLevelEnum(logLevelName: string): logLevelEnum {
/**
* we need the correct type so that typescript will allow the index access
* note: we must cast the string to `keyof typeof logLevelEnum`
* which resolves to: "NOTHING" | "ERROR" | "WARN" | "INFO" | "DEBUG"
* i.e. all valid enum-names
*/
const enumName = logLevelName as keyof typeof logLevelEnum;
/**
* now that we have the correct type of the enum-name, we can get the enum-value
*/
const configEnum = logLevelEnum[enumName];
/**
* keep in mind, that we were cheating a little bit in the type-expression above
* We told typesript that we are sure that enumName is a valid enum-name,
* but actually it is just the value of the logLevelName string, which could be anything.
* Thus, typescript now thinks that configEnum is of type logLevelEnum, but
* actually it is `logLevelEnum | undefined` (undefined when logLevelEnum is not a valid enum-name)
*
* This is the reason why use the nullish coalescing operator (??):
* see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing
*
* so we return configEnum, or logLevelEnum.NOTHING (when configEnum is undefined)
*/
return configEnum ?? logLevelEnum.NOTHING;
}

Related

Object deconstruction with exclamation mark after object [duplicate]

When looking at the sourcecode for a tslint rule, I came across the following statement:
if (node.parent!.kind === ts.SyntaxKind.ObjectLiteralExpression) {
return;
}
Notice the ! operator after node.parent. Interesting!
I first tried compiling the file locally with my currently installed version of TS (1.5.3). The resulting error pointed to the exact location of the bang:
$ tsc --noImplicitAny memberAccessRule.ts
noPublicModifierRule.ts(57,24): error TS1005: ')' expected.
Next I upgraded to the latest TS (2.1.6), which compiled it without issue. So it seems to be feature of TS 2.x. But the transpilation ignored the bang completely, resulting in the following JS:
if (node.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) {
return;
}
My Google fu has thus far failed me.
What is TS's exclamation mark operator, and how does it work?
That's the non-null assertion operator. It is a way to tell the compiler "this expression cannot be null or undefined here, so don't complain about the possibility of it being null or undefined." Sometimes the type checker is unable to make that determination itself.
It is explained in the TypeScript release notes:
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.
I find the use of the term "assert" a bit misleading in that explanation. It is "assert" in the sense that the developer is asserting it, not in the sense that a test is going to be performed. The last line indeed indicates that it results in no JavaScript code being emitted.
Louis' answer is great, but I thought I would try to sum it up succinctly:
The bang operator tells the compiler to temporarily relax the "not null" constraint that it might otherwise demand. It says to the compiler: "As the developer, I know better than you that this variable cannot be null right now".
Non-null assertion operator
With the non-null assertion operator we can tell the compiler explicitly that an expression has value other than null or undefined. This is can be useful when the compiler cannot infer the type with certainty but we have more information than the compiler.
Example
TS code
function simpleExample(nullableArg: number | undefined | null) {
const normal: number = nullableArg;
// Compile err:
// Type 'number | null | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'.(2322)
const operatorApplied: number = nullableArg!;
// compiles fine because we tell compiler that null | undefined are excluded
}
Compiled JS code
Note that the JS does not know the concept of the Non-null assertion operator since this is a TS feature
"use strict";
function simpleExample(nullableArg) {
const normal = nullableArg;
const operatorApplied = nullableArg;
}
Short Answer
Non-null assertion operator (!) helps the compiler that I'm sure this variable is not a null or undefined variable.
let obj: { field: SampleType } | null | undefined;
... // some code
// the type of sampleVar is SampleType
let sampleVar = obj!.field; // we tell compiler we are sure obj is not null & not undefined so the type of sampleVar is SampleType
My understanding is the ! operator do the same thing like NonNullable.
let ns: string | null = ''
// ^? let ns: string | null
let s1 = ns!
// ^? let s1: string
let s2 = ns as NonNullable<typeof ns>
// ^? let s2: string

Is it possible to let the interpreter know my method checks for undefined/null in TypeScript

For example I have a method isEmpty that checks if anything is empty, null, or undefined and returns true if so.
However in TypeScript it doesn't let the interpreter know that this is the case and I get a red underline in my IDE (WebStorm)
Example code
let str: string | undefined = undefined
if (!isEmpty(str)) {
doSomeWorkFunction(str) // this line is shows the error 'string | undefined is not assignable to type string
}
However if the code is
let str: string | undefined = undefined
if (str) {
doSomeWorkFunction(str) // no error because the interpreter knows I checked the value
}
The fix I would like to avoid is
let str: string | undefined = undefined
if (!isEmpty(str)){
// #ts-ignore
doSomeWorkFunction(str) // no error since ts is now ignoring this error
}
How might I go about still keeping the TypeScript strict null checks in place without having to ignore issues like this.
TypeScript has a feature called "type guards" that helps in this situation: https://www.typescriptlang.org/docs/handbook/advanced-types.html. Specifically, it lets you tell the compiler that the return type is not just a boolean, but a boolean that means something specific about the types of the inputs. For example, you can convert a function like this
function isDefinedString(input: string | undefined): boolean
into a function like this:
function isDefinedString(input: string | undefined): input is string
The return type is still a boolean, but now the compiler will assume that the input is specifically a string and not any other type allowed by the argument declaration (in this case undefined).
Try using this signature on your existing isEmpty function declaration. Although not required to make it work, because you are adding this additional context to the function signature I'd recommend changing the name of isEmpty to reflect its dual purpose of checking emptiness and whether the variable is defined.
Edit:
One caveat to returning type information is that returning false will make the compiler assume that the object is not that type. In the above example, if isDefinedString returns false then the compiler will assume that it is not a string. This runs into problems with any or generic parameters, because returning false effectively tells the compiler that there is no type (or in the compiler's words, there is "never" a type) that satisfies your criteria. While this doesn't result in an error directly, the fact that the compiler has no type that works with your object means you can't do anything meaningful with the object in the if/else branch triggered by your type guard returning false. As such, if you are using a broad type such as any or a generic, you will want to limit what your type guard says to something like input is (null | undefined) or input is MySpecificInterface if you plan to do something meaningful in both true and false cases. This trickiness may also be a sign that you want to separate your validation into two checks:
if(typeGuard(myObject)) {
if(isValid(myObject)) {
// do something with valid object
} else {
// do something with invalid object
}
}
// do nothing without an object to act upon

What is the purpose of the "!" in this TypeScript method call? [duplicate]

When looking at the sourcecode for a tslint rule, I came across the following statement:
if (node.parent!.kind === ts.SyntaxKind.ObjectLiteralExpression) {
return;
}
Notice the ! operator after node.parent. Interesting!
I first tried compiling the file locally with my currently installed version of TS (1.5.3). The resulting error pointed to the exact location of the bang:
$ tsc --noImplicitAny memberAccessRule.ts
noPublicModifierRule.ts(57,24): error TS1005: ')' expected.
Next I upgraded to the latest TS (2.1.6), which compiled it without issue. So it seems to be feature of TS 2.x. But the transpilation ignored the bang completely, resulting in the following JS:
if (node.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) {
return;
}
My Google fu has thus far failed me.
What is TS's exclamation mark operator, and how does it work?
That's the non-null assertion operator. It is a way to tell the compiler "this expression cannot be null or undefined here, so don't complain about the possibility of it being null or undefined." Sometimes the type checker is unable to make that determination itself.
It is explained in the TypeScript release notes:
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.
I find the use of the term "assert" a bit misleading in that explanation. It is "assert" in the sense that the developer is asserting it, not in the sense that a test is going to be performed. The last line indeed indicates that it results in no JavaScript code being emitted.
Louis' answer is great, but I thought I would try to sum it up succinctly:
The bang operator tells the compiler to temporarily relax the "not null" constraint that it might otherwise demand. It says to the compiler: "As the developer, I know better than you that this variable cannot be null right now".
Non-null assertion operator
With the non-null assertion operator we can tell the compiler explicitly that an expression has value other than null or undefined. This is can be useful when the compiler cannot infer the type with certainty but we have more information than the compiler.
Example
TS code
function simpleExample(nullableArg: number | undefined | null) {
const normal: number = nullableArg;
// Compile err:
// Type 'number | null | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'.(2322)
const operatorApplied: number = nullableArg!;
// compiles fine because we tell compiler that null | undefined are excluded
}
Compiled JS code
Note that the JS does not know the concept of the Non-null assertion operator since this is a TS feature
"use strict";
function simpleExample(nullableArg) {
const normal = nullableArg;
const operatorApplied = nullableArg;
}
Short Answer
Non-null assertion operator (!) helps the compiler that I'm sure this variable is not a null or undefined variable.
let obj: { field: SampleType } | null | undefined;
... // some code
// the type of sampleVar is SampleType
let sampleVar = obj!.field; // we tell compiler we are sure obj is not null & not undefined so the type of sampleVar is SampleType
My understanding is the ! operator do the same thing like NonNullable.
let ns: string | null = ''
// ^? let ns: string | null
let s1 = ns!
// ^? let s1: string
let s2 = ns as NonNullable<typeof ns>
// ^? let s2: string

Verifying value isn't undefined in typescript inside a variable doesn't work

I have the following code:
interface A {
data?: number
}
const acceptsNumber = (x: number) => {
console.log(x)
}
const a: A = {
data: 6
}
const doesAHasValue = a.data
if (doesAHasValue) {
acceptsNumber(a.data)
}
And I get the following error for the last line(acceptsNumber(a.data)):
Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
Type 'undefined' is not assignable to type 'number'. TS2345
But if I change the condition to
if (a.data) {
acceptsNumber(a.data)
}
It transpiles fine. Why?
I know I can solve it by acceptsNumber(doesAHasValue) but I want to know why it doesn't transpile in the first place?
You get the error because the type of a.data is number | undefined but the function requires a number.
However, when you do:
if(a.data) { ... } which resolves to true, TypeScript creates a "type-guard" and implicitly changes the type of a.data just inside the { ... } to be number.
Once the type is number you get no errors.
You can read more about Type Guards here.
The issue which you are having is because TS is not able to follow relation between doesAHasValue and a.data. You indeed as a human look at the code and see this relation, as line before the condition you made the assignment - const doesAHasValue = a.data, and it looks obvious to see the relation. But for TS there is none, TS sees doesAHasValue as unique value, with type number | undefined and if you ask if (doesAHasValue) the only thing we are narrowing is exactly doesAHasValue to number(BTW that is why also acceptsNumber(doesAHasValue) works).
For TS assignment is only saying - I have the same type as the value on the right has. It is not saying - I am only alias bound to the original value. doesAHasValue and a.data share the same type and value, but are not bound together, checking one does not effect type of the second.
In your particular example such behavior of TS has a great sense, as we can mutate a.data after assignment. Check below code, nothing protects us from having different value in both places:
const doesAHasValue = a.data
a.data = undefined; // we set it undefined, doesAHasValue is still 6
if (doesAHasValue) {
acceptsNumber(a.data) // a.data is undefined here
}
But for situation when we have real alias, and we use const, even then still TS is not connecting types. Consider:
const f = (arg: number | undefined) => {
const a = arg;
const b = a;
if (b) {
b // number
a // number | undefined even though b is alias for a, so it theoretically could be narrowed
}
}
In summary, compilator is not able to catch the connection between the original value a.data and the new one doesAHasValue. I assume tracking such relation would be overkill for compilation.

What does exclamation mark "!." after an object property means in Javascript [duplicate]

When looking at the sourcecode for a tslint rule, I came across the following statement:
if (node.parent!.kind === ts.SyntaxKind.ObjectLiteralExpression) {
return;
}
Notice the ! operator after node.parent. Interesting!
I first tried compiling the file locally with my currently installed version of TS (1.5.3). The resulting error pointed to the exact location of the bang:
$ tsc --noImplicitAny memberAccessRule.ts
noPublicModifierRule.ts(57,24): error TS1005: ')' expected.
Next I upgraded to the latest TS (2.1.6), which compiled it without issue. So it seems to be feature of TS 2.x. But the transpilation ignored the bang completely, resulting in the following JS:
if (node.parent.kind === ts.SyntaxKind.ObjectLiteralExpression) {
return;
}
My Google fu has thus far failed me.
What is TS's exclamation mark operator, and how does it work?
That's the non-null assertion operator. It is a way to tell the compiler "this expression cannot be null or undefined here, so don't complain about the possibility of it being null or undefined." Sometimes the type checker is unable to make that determination itself.
It is explained in the TypeScript release notes:
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.
I find the use of the term "assert" a bit misleading in that explanation. It is "assert" in the sense that the developer is asserting it, not in the sense that a test is going to be performed. The last line indeed indicates that it results in no JavaScript code being emitted.
Louis' answer is great, but I thought I would try to sum it up succinctly:
The bang operator tells the compiler to temporarily relax the "not null" constraint that it might otherwise demand. It says to the compiler: "As the developer, I know better than you that this variable cannot be null right now".
Non-null assertion operator
With the non-null assertion operator we can tell the compiler explicitly that an expression has value other than null or undefined. This is can be useful when the compiler cannot infer the type with certainty but we have more information than the compiler.
Example
TS code
function simpleExample(nullableArg: number | undefined | null) {
const normal: number = nullableArg;
// Compile err:
// Type 'number | null | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'.(2322)
const operatorApplied: number = nullableArg!;
// compiles fine because we tell compiler that null | undefined are excluded
}
Compiled JS code
Note that the JS does not know the concept of the Non-null assertion operator since this is a TS feature
"use strict";
function simpleExample(nullableArg) {
const normal = nullableArg;
const operatorApplied = nullableArg;
}
Short Answer
Non-null assertion operator (!) helps the compiler that I'm sure this variable is not a null or undefined variable.
let obj: { field: SampleType } | null | undefined;
... // some code
// the type of sampleVar is SampleType
let sampleVar = obj!.field; // we tell compiler we are sure obj is not null & not undefined so the type of sampleVar is SampleType
My understanding is the ! operator do the same thing like NonNullable.
let ns: string | null = ''
// ^? let ns: string | null
let s1 = ns!
// ^? let s1: string
let s2 = ns as NonNullable<typeof ns>
// ^? let s2: string

Categories

Resources