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

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.

Related

Argument of type 'string | number | string[] | undefined' is not assignable to parameter of type 'string' when converting JavaScript to TypeScript

I'm trying to covert the following JavaScript function to TypeScript and I'm getting the following error
$("#checkoutSubmit").click(function () {
var departureDate = $("#bookingDepartureDateInput").val();
localStorage.setItem("datevalue", departureDate);
console.log(localStorage.getItem("datevalue"))
});
I've never used TypeScript and I seem to be struggling with converting this. Can I get some insight on how to rewrite this code in TypeScript? Thank you.
the JQuery val(); method can return a String, Number or Array<string> this causes an error because localStorage.setItem only accepts strings.
If you are sure that $("#bookingDepartureDateInput").val(); always returns a string you can use as string to tell typescript that it can assume it will be a string:
$("#checkoutSubmit").click(function () {
const departureDate = $("#bookingDepartureDateInput").val() as string;
localStorage.setItem("datevalue", departureDate);
console.log(localStorage.getItem("datevalue"))
});
JQuery .val() docs.
The jQuery val method returns string, string[], number, or undefined. But you're trying to use it with the storage interface's setItem method, which only accepts strings. So you get an error, since that may be a mistake.
If you know that the value from val will be a string, while you could just do as string on it, the problem with that is you're making an assumption that you aren't checking: that nothing will make that claim (a type assertion) untrue. Instead, you could use a type assertion function to reassure TypeScript and ensure that the condition is correct:
function assertIsString(value: any): asserts value is string {
if (typeof value !== "string") {
throw new Error(`String expected, got non-string instead`);
}
}
The the code would be:
$("#checkoutSubmit").click(function () {
const departureDate = $("#bookingDepartureDateInput").val();
assertIsString(departureDate);
localStorage.setItem("datevalue", departureDate);
console.log(localStorage.getItem("datevalue"))
});
That works because after the assertion function call, TypeScript knows it has a 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

Need help parsing TypeScript syntax

I'm new to TypeScript and can't mentally parse its syntax -- I'm a backend engineer, so I don't spend much time with JavaScript/JavaScripty-languages. I've tried searching, but I can't find anything that details what I'm seeing (and because I'm new to the language, I'm probably not using the correct terms).
In the following code, what am I looking at?
/**
* `makeStyles` where the passed `styles` do not depend on props
*/
export default function makeStyles<Theme = DefaultTheme, ClassKey extends string = string>(
style: Styles<Theme, {}, ClassKey>,
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>
): (props?: any) => ClassNameMap<ClassKey>;
https://github.com/mui-org/material-ui/blob/9bd4277ecd660ebe2fd4cb08960f58e98ddb6d43/packages/material-ui-styles/src/makeStyles/makeStyles.d.ts
I understand the export default function funcName, but after that I'm less sure. This is what my brain sees:
// default named export for renaming hijinks
export default function
// function name, type annotated argument list?
makeStyles<Theme = DefaultTheme, ClassKey extends string = string>
(
// property being passed to property name with typed annotation on argument?
style: Styles<Theme, {}, ClassKey>,
// 'options' are optional in the interface, so when passing this argument we also have to include '?' ?
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>
// if there are any 'props' (which are optional in the interface), we bind as props on the return type of 'ClassNameMap'
): (props?: any) => ClassNameMap<ClassKey>;
but that's probably wrong. Can you help me understand the components or help with the vocabulary needed to learn?
It is a lot to unpack, here it is line by line.
export default function
Your spot on about this line. Just exporting the function to make it available elsewhere in the program.
makeStyles<Theme = DefaultTheme, ClassKey extends string = string>
Here makeStyles is the name of the function.
What's in between the <> is the generic arguments that it takes. Typescript Generics let you write functions that can take a variety of types as arguments. For example:
function identity<T>(arg: T): T {
return arg;
}
The argument here is arg: T, which means arg must be of type T, and the return is typed with (): T, this second :T after the closing bracket indicates the return type of the function. So the argument and the return of this function must be of the same type.
So in our example the makeStyles function takes a generic argument of Theme and ClassKey. Generics also let you specify default values for these arguments if they aren't explicitly passed in. So where you see:
<Theme = DefaultTheme, ClassKey extends string = string>
If the values are not defined when the function is invoked, then Theme will be of type DefaultTheme, and ClassKey will be of type string.
The other piece there is that ClassKey extends string. That just means that it inherits all the string properties within it's type.
Then we have the arguments themselves here:
(
style: Styles<Theme, {}, ClassKey>,
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>
)
style is the first argument. It's of type Styles which is also a generic type that takes three arguments. You may want to look up the Styles interface or type to see what that looks like to get more clarification. Here is an example of what it could look like:
interface Styles<ThemeType, AppType, ClassKeyType> {
theme: ThemeType;
app: AppType;
classKey: ClassKeyType;
.... a bunch of other properties for the styles type.
}
With Generics you can pass in a variety of types, which makes things more flexible. And the typescript compiler is smart enough to enforce this within your code depending on how things were invoked or instantiated.
options is the second argument. You are correct about the ?, that signifies that it is an optional argument. If it is not passed in, it will just be undefined. Here we are using a special typescript type called Omit. Omit will take a type as it's first argument, and a key of that type as the second argument. It will return a new type that has all the properties of the original type you passed in as the first argument, EXCEPT, the key that you passed in as the second argument.
Here WithStyleOptions is another generic type, like styles. It takes Theme as it's argument. From there Omit will take that type, and produce a new type, and exclude the withTheme property.
Then we have:
: (props?: any) => ClassNameMap<ClassKey>;
This last piece is typing the return type of the function. In this case, this function will return another function.
The new function it returns will take one argument named props which will have a type of any. any in typescript means what you would think, it could literally be anything. This function will have a return type of ClassNameMap, which is another generic.
For sure take a look at all the generics you are using here, and see how they are defined. That should help. Also see where the makeStyle function is called, I think seeing it from the other side will also be helpful.
This is just a type defintion (everything in the .d.ts file should just be definitions).
Everything is typing.
The angle brackets <> after the function name indicate it is a generic function, which means you can specify a specifically typed instance of makeStyles. The = is for providing default types.
Next is the parameters, surrounded by (). These are just named parameters with their type annotations. Yes, ? indicates the parameter is optional. Omit is a utility type that will drop the specified key from the type.
The : after the parentheses is for the return type. In this case, this function returns a function. The syntax for returning a function is (parameter: parameterType) => returnType.
// default named export
export default function
// generic function with default types
makeStyles<Theme = DefaultTheme, ClassKey extends string = string>
(
// parameter with typed annotation
style: Styles<Theme, {}, ClassKey>,
// option parameter, Omit is a TypeScript global utility,
// takes everything but 'withTheme' from WIthStylesOptions<Theme>
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>
// returns a function that takes props and returns a ClassNameMap<ClassKey>
): (props?: any) => ClassNameMap<ClassKey>;

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.

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