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

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.

Related

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

Using `enum` in ternary operator TypeScript

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;
}

How to deduce the type of function argument by value of previous argument?

Using typescript, I want the compiler/IDE to deduce the type of argument when it can. How do I make it work ?
First argument of my function is a string and it's value will decide the type of data that can be passed as the second argument. But I am not able to do that. I am sharing how I expected the compiler to work in this case but it is not helping me.
interface AuxType {
name: string,
user: number
}
type ValueType = AuxType[keyof AuxType]
function run(key: string, value: ValueType) {
return dostuff(key, value)
}
run('name', "Stack") // Works as expected
run('user', 2) // Works as expected
run('name', 2) // Expecting this to error but it is not
run('user', "Stack") // Expect this to error but it works in typescript
Is it even possible in typescript ? Shouldn't this be possible with string literal value of the first argument ?
You need to use a generic. Right now, you are just defining ValueType as the union of the types of name and user, i.e. string | number, but it has no dependency on which key is actually passed to the function. To make the key and value depend you need to use a generic with your function, like this:
function run<T extends keyof AuxType>(key: T, value: AuxType[T]) {
return dostuff(key, value)
}
Now the key must be a keyof AuxType (thats what the extends is doing) and the value must be the corresponding type of that key in AuxType.

Declared a type with Typescript but Javascript internally works with a wrong type

I'm quite new to Typescript and work through a education video. Yesterday I found a weird behavior and think this is a bug.
Example:
const json = '{"x": 10, "y":10}';
const coordinates: { x: number; y: number } = JSON.parse(json);
console.log(typeof coordinates.y);
This normally outputs x and y as type number. But it's not because of the type declaration, it's because of the JSON value.
If you declare one of them as String, VS Code treats it like a String but internally it stays a number:
const json = '{"x": 10, "y":10}';
const coordinates: { x: number; y: string } = JSON.parse(json);
console.log(typeof coordinates.y);
This is the code:
As you see, VS Code treats it as a string
But the type checking proves that this isn't correct.
In my opinion it makes sense that an Object/Array after parsing has an any type. but it should be either message you an error if the parsed JSON-value is different to your annotation or it should morph the given value exact to the annotation.
I'm quite new to this, so if my assumption is wrong, please let me know!
Typescript does inference before and during compilation. After compilation to JS, if some of your code's type logic is unsound, some of the types you've declared may be invalid. This is what's happening here.
JSON.parse returns something of the type any, which can be literally anything. You can try to extract properties from it with the line:
const coordinates: { x: number; y: string } = JSON.parse(json);
But this does not mean that the parsed object will actually have an x property as a number, or a y property as a string - your code there is telling the compiler to assume that it does, and to treat y as a string later in the code.
If you have something which is, by default, of type any, when extracting properties from it, you should make sure that its properties are really is of the type you're denoting them to be. If you're not 100% sure the properties will always be as expected (for example, if the input string comes from a network response - what if the response is 503?), make a type guard, such as:
const isCoordinates = (param: unknown): param is { x: number, y: number } => {
return typeof param === 'object' && param !== null && 'x' in param;
};
Then call isCoordinates before trying to extract properties from the string to make sure it's of the format you expect first.
Otherwise, like what's happening here, you can tell the compiler something false, and bugs and strange things may happen as a result.
You may consider enabling the tslint rule no-unsafe-any to avoid making these sort of mistakes.

Flow - maybe type incompatible with union types

Flow code can be run here.
Using flow, I have a function that takes a key value pair object and gets a value for it - the value it gets should be a string, number or boolean.
type ValueType = string | number | bool | null | void;
type ObjectOfValues = {[string]: ValueType}
function getValueFromObjectOfValues(objectOfValues: ObjectOfValues, name: string): ValueType {
return objectOfValues[name];
}
I define some object type that has a property that's a maybe string:
type SomeValueWithNullableString = {
someProperty: ?string
}
Then I create a function that takes my specific object type and calls the function to get a value from it:
function getValue (someObject: SomeValueWithNullableString) {
return getValueFromObjectOfValues(someObject, 'someProperty');
}
This results in a flow error:
type ObjectOfValues = {[string]: ValueType}
^ boolean. This type is incompatible with the expected param type of someProperty:
?string ^ string 2: type ObjectOfValues =
{[string]: ValueType}
^ number. This type is incompatible with the expected param type of 9: someProperty:
?string ^ string
What am I doing wrong?
The problem with this code is that the objects are mutable, so getValueFromObjectOfValues could legally do objectOfValues.someProperty = 5.
If Flow allowed this subtyping relationship, then the original caller, who thought they had an object where someProperty had type ?string, would now have an object where someProperty had type number, thereby breaking the type system.
To solve this problem, you can use property variance. You need to change your type like this:
type ObjectOfValues = {+[string]: ValueType}
This means, loosely, that if you have an object of type ObjectOfValues, all you know is that its properties are some subtype of ValueType. This means that when you read from them, you will get a ValueType. But Flow won't let you write to them, since it doesn't know what type they actually are -- just that they are a subtype of ValueType.

Categories

Resources