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

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

Related

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.

Argument of type 'string | undefined' is not assignable to parameter of type 'string' - TypeScript error

TS error "Object is undefined"
Trying to access "userid" from my headers.
It keeps throwing the error "Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
Type 'undefined' is not assignable to type 'string'."
When I have already defined userid as string or undefined why does this error pop up??
Any idea anyone????
Update:
Adding a check before accessing apiGateway event
if (apiGateway.event !== undefined) {
const { userid } = req.apiGateway.event.headers;
} else {
throw new badRequestException()
}
return......
Judging by your definition of MyRequest:
apiGateway can be undefined
event cannot be undefined
headers cannot be undefined
userid can be undefined
So right off the bat, writing req.apiGateway.event.headers will fail, because if apiGateway is undefined, then accessing .event on it will crash. So Typescript does not allow that.
There are three ways around this:
Explicitly check against it. Something like if (req.apiGateway === undefined) { throw new badRequestException(); }
If you're certain that it can't be undefined, you can force TypeScript to accept it. Write req.apiGateway!.event. Mind you, if apiGateway does happen to be undefined at runtime, you'll get an exception. So only use this if you are absolutely 100% sure that it cannot under any circumstance be undefined.
Accept that it can be undefined and coerce everything else to undefined as well. Write req.apiGateway?.event - in this case .event will also be considered as undefined-able. If req.apiGateway happens to be undefined in runtime, then req.apiGateway?.event will also be undefined. And so on and so forth. This means that you'll also have to add ?. to the rest of the line: req.apiGateway?.event?.headers. And the result of the whole expression is ALSO undefined-able, so you'll probably need to use more undefined-checks later.
Now, for your second problem. Currently your userid local variable has the type string|undefined. That us because req.apiGateway.event.headers.userid is string|undefined. This means that at runtime it can be either a string, or undefined. However the method updateNotification() expects a simple string as its parameter. It cannot deal with undefined values. If you were to pass it undefined, who knows what would happen (probably an exception). Therefore Typescript does not allow this. And, again, you can use the above methods for dealing with it.
always when you got this error it's good to just check if variable has some value:
if(!variable) return
// continue
after this if type of variable will be string instead of string | undefined

How to "fix" flow types when you know from the logic that the type is correct?

Well considering a function that takes/creates a maybe type (for say a number); Then another function that doesn't take this maybe type. To make it "work" I guard the function that doesn't take a maybe-type by adding a conditional around it.
A simple example:
/* #flow */
export function nullOrUndefined(val: mixed): boolean {
return val === null || val === undefined;
}
function foo(x: ?number) {
console.log(!nullOrUndefined(x) ? addOne(x) : null);
}
function addOne(x: number) {
return x + 1;
}
The nullOrUndefined would be a generic guard, I created it to have a simple utility function that is expressive, so I don't have to constantly type the "complex" test in line.
The above functionality would work, and would not throw an error. (So long as foo receives a number, undefined or null.
However flow gives the following error:
8: console.log(!nullOrUndefined(x) ? addOne(x) : null);
^ Cannot call `addOne` with `x` bound to `x` because null or undefined [1] is incompatible with number [2].
References:
7: function foo(x: ?number) {
^ [1]
12: function addOne(x: number) {
^ [2]
A small test
I understand why this error occurs (flow can't look into any arbitrary function, and nullOrUndefined wouldn't even be in the same file.
But, how can I fix this? Other than // $FlowFixMe ? Or is this the case where explicit "ignore line" is correct usage?
Ah, flow has first-class support for your case. Your error can be resolved by the addition of one token: %checks.
export function nullOrUndefined(val: mixed): boolean %checks {
...
(try link)
%checks is used to indicate to flow that the indicated function is a type refinement predicate. Be advised, refinement in flow is pretty basic and will be very easily confused by a function basically any more complex than yours.

Explain generics using Javascript's Flowtype

I have never written in statically typed language before. I'm mostly developing in Javascript and lately I've been interested in learning more about FB's Flowtype.
I find the documentation nicely written and I understand most of it. However I don't quite get the concept of generics. I've tried googling some examples / explanations but with no luck.
Could someone please explain what generics are, what are they mostly used for and perhaps provide an example?
Let's say I want to write a class that just stores a single value. Obviously this is contrived; I'm keeping it simple. In reality this might be some collection, like an Array, that can store more than one value.
Let's say I need to wrap a number:
class Wrap {
value: number;
constructor(v: number) {
this.value = v;
}
}
Now I can create an instance that stores a number, and I can get that number out:
const w = new Wrap(5);
console.log(w.value);
So far so good. But wait, now I also want to wrap a string! If I naively just try to wrap a string, I get an error:
const w = new Wrap("foo");
Gives the error:
const w = new Wrap("foo");
^ string. This type is incompatible with the expected param type of
constructor(v: number) {
^ number
This doesn't work because I told Flow that Wrap just takes numbers. I could rename Wrap to WrapNumber, then copy it, call the copy WrapString, and change number to string inside the body. But that is tedious and now I have two copies of the same thing to maintain. If I keep copying every time I want to wrap a new type, this will quickly get out of hand.
But notice that Wrap doesn't actually operate on the value. It doesn't care whether it is number or string, or something else. It only exists to store it and give it back later. The only important invariant here is that the value you give it and the value you get back are the same type. It doesn't matter what specific type is used, just that those two values have the same one.
So, with that in mind we can add a type parameter:
class Wrap<T> {
value: T;
constructor(v: T) {
this.value = v;
}
}
T here is just a placeholder. It means "I don't care what type you put here, but it's important that everywhere T is used, it is the same type." If I pass you a Wrap<number> you can access the value property and know that it is a number. Similarly, if I pass you a Wrap<string> you know that the value for that instance is a string. With this new definition for Wrap, let's try again to wrap both a number and a string:
function needsNumber(x: number): void {}
function needsString(x: string): void {}
const wNum = new Wrap(5);
const wStr = new Wrap("foo");
needsNumber(wNum.value);
needsString(wStr.value);
Flow infers the type parameter and is able to understand that everything here will work at runtime. We also get an error, as expected, if we try to do this:
needsString(wNum.value);
Error:
20: needsString(wNum.value);
^ number. This type is incompatible with the expected param type of
11: function needsString(x: string): void {}
^ string
(tryflow for the full example)
Generics among statically typed languages are a method of defining a single function or class that can be applied to any type dependency instead of writing a separate function/class for each possible data type. They ensure that the type of one value will always be the same at the type of another that are assigned to the same generic value.
For example, if you wanted to write a function that added two parameters together, that operation (depending on the language) could be entirely different. In JavaScript, since it is not a statically typed language to begin with, you can do this anyway and type check within the function, however Facebook's Flow allows for type consistency and validation in addition to single definitions.
function add<T>(v1: T, v2: T): T {
if (typeof v1 == 'string')
return `${v1} ${v2}`
else if (typeof v1 == 'object')
return { ...v1, ...v2 }
else
return v1 + v2
}
In this example we define a function with a generic type T and say that all parameters will be of the same type T and the function will always return the same type T. Inside of the function since we know that the parameters will always be of the same type, we can test the type of one of them using standard JavaScript and return what we perceive and "addition" for that type to be.
When in use later in our code, this function can then be called as:
add(2, 3) // 5
add('two', 'three') // 'two three'
add({ two: 2 }, { three: 3 }) // { two: 2, three: 3 }
But will throw typing errors if we attempt:
add(2, 'three')
add({ two: 2 }, 3)
// etc.
Basically, it's just a placeholder for a type.
When using a generic type, we are saying that any Flow type can be used here instead.
By putting <T> before the function arguments, we're saying that this function can (but doesn't have to) use a generic type T anywhere within its arguments list, its body, and as its return type.
Let's look at their basic example:
function identity<T>(value: T): T {
return value;
}
This means that the parameter value within identity will have some type, which isn't known in advance. Whatever that type is, the return value of identity must match that type as well.
const x: string = identity("foo"); // x === "foo"
const y: string = identity(123); // Error
An easy way to think about generics is to imagine one of the primitive types instead of T and see how that would work, then understand that this primitive type can be substituted for any other.
In terms of identity: think of it as a function that accepts a [string] and returns a [string]. Then understand that [string] can be any other valid flow type as well.
This means identity is a function that accepts T and returns a T, where T is any flow type.
The docs also have this helpful analogy:
Generic types work a lot like variables or function parameters except that they are used for types.
Note: Another word for this concept is polymorphism.

Categories

Resources