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

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.

Related

which is the best way to define an object in typescript and access it values?

I have a Object of format
export const IconSizeMap = {
'extra-small': '0.75rem',
small: '1rem',
medium: '1.5rem',
large: '2rem',
'extra-large': '4rem'
};
Being new to typescript i could not understand what the error is
It gives an error
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ 'extra-small': string; small: string; medium: string; large: string; 'extra-large': string; }'.
No index signature with a parameter of type 'string' was found on type '{ 'extra-small': string; small: string; medium: string; large: string; 'extra-large': string; }'.ts(7053)
when i try to use
IconSizeMap[size]
How do i solve this error ?
You're getting this error because your tsconfig.json has the "noImplicitAny": true enabled. It means that TypeScript will look for and inform you of places where you use a value whose type is not known, and where you did not explicitly tell him which type it should be.
Your solutions to get rid of it are either to :
Disable noImplicitAny in the configuration (But I guess there's a reason it's here so I wouldn't recommend that)
Tell TypeScript the type of what you're using that causes the error.
Here, TypeScript recognized each of the 5 keys you defined (extra-small, small, medium, large and extra-large) because you assigned them a value that is a string, so it can tell they're strings. However, only those 5 strings are keys.
When you're using a "generic" string to access your object such as what I assume your size is, you're not sure whether it corresponds to one of the 5 keys. So, TypeScript warns you of that.
One solution to that could be to tell the compiler you're sure about what you're doing, and specifying the type of size more precisely. For example :
export const IconSizeMap = {
'extra-small': '0.75rem',
small: '1rem',
medium: '1.5rem',
large: '2rem',
'extra-large': '4rem'
};
let size: keyof typeof IconSizeMap; // Means "I'm sure that size is one of those strings
size = "small";
console.log(IconSizeMap[size]);
Playground link
IconSizeMap[size] is trying to use a string as an index on an object that isn't defined with string indexing, which is why you're getting that error.
Your options are:
Add a string index signature to the object, or
Don't use an general string to index into the object
Which you do is up to your use case. An index signature means TypeScript can't proactively check for you that size matches a property on the object. But using only specific property names means listing those property names in more than one place if your starting point is a string that could have any value.
Here's an example of #1:
export const IconSizeMap: {[key: string]: string} = {
// −−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^
'extra-small': '0.75rem',
small: '1rem',
medium: '1.5rem',
large: '2rem',
'extra-large': '4rem'
};
Playground link
Here's an example of #2:
function getIconSize(size: string) {
switch (size) {
case 'extra-small':
return IconSizeMap['extra-small'];
case 'small':
return IconSizeMap.small;
case 'medium':
return IconSizeMap.medium;
case 'large':
return IconSizeMap.large;
case 'extra-large':
return IconSizeMap['extra-large'];
default:
throw new Error(`Invalid 'size' for IconSizeMap: ${size}`);
}
}
Notice how we use specific string literals, not just any string, when accessing the object.
Playground link
Since T.J. Crowder's answer is correct, I'll just add some comments in case it helps understand better what is going on.
You have to remember that TypeScript adds static typing to JavaScript. That means that every entity defined in the language has a definite type, whether defined by you or inferred implicitly by TypeScript, and TypeScript will enforce observance of that type.
In your case, TypeScript implicitly infers that your IconSizeMap object is of a definite type. It's not a generic JavaScript object which can have any string key, but an object with some definite keys: extra-small, small, etc. TypeScript infers that from your definition of the object.
Once TypeScript has inferred a type for an entity, it will enforce it for every use you make of that entity. That means that if you try something like IconSizeMap['huge'] it will complain, since 'huge' was not registerd as a valid key in the type inferred by TypeScript.
This is quite easy to understand since 'huge' is explictly not contained in the object. But it has trickier consequences, like the problem you mentioned. If you are using variable size of type string to access the object properties, TypeScript will also complain because it can be possible that the value of size be a string not registered as one of the object keys. That's the reason for T.J.'s solutions:
You either explicitly define the object's type as allowing any string key.
You restrict the type of your string variables that will refer to object keys, so that they cannot be any string but just one of the strings contained in your object.
I suppose the second solution is what you need, since your object seems to be a fixed map, that doesn't allow further string keys. You can let TypeScript infer that implicitly, as in T.J's solution that uses a switch with only the expected string literals.
However, in situations like this I sometimes explicitly define union types containing those string literals. For example:
type IconSize = 'extra-small' | 'small' | 'medium' | 'large ' | 'extra-large';
I can then define variables explicitly with that type, and TypeScript will help me avoid assigning different string values to that variable:
let size: IconSize;
This means that TypeScript will rule out any attempt at setting a different string value to size. Since size will have one of the expected values, the next time you try IconSizeMap[size] TypeScript will not complain.

typescript: declare type of map-like object

I'm using an object to store a map, keys are strings, values have a fixed type T.
When looking up a key in the object, the type inference assigns it the type T.
But it might be undefined.
In the following example, I would expect entry to have the type number|undefined. But typescript infers the type number. That doesn't seem correct:
const data: {[index:string]: number} = {
"aa34da": 1,
"basd23": 2,
"as34sf": 5
};
const entry = data["doesn't exist"];
console.log(entry);
Is this a bug in the type inference?
I am aware of the ES6 Map, which offers a get() method of exactly the signature that I would expect. But Map doesn't play well with JSON serialization. I would prefer to just use objects.
As you've already figured out, there's no bug in type inference because the data's type is explicitly set to "object containing number at any string key". So we should let typescript know that value might be undefined:
declare const data: { [index: string]: number|undefined };
You could also use Record utility to define such a type:
declare const data: Record<string, number | undefined>;
Record<K, T> Constructs a type with a set of properties K of type T.
After some playing around with the code, I think the proper way to declare a map-like object, where some keys might not point to values is:
const data: {[index:string]: number|undefined} = {
"aa34da": 1,
"basd23": 2,
"as34sf": 5
}
const entry = data["doesn't exist"];

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.

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.

How to force key types in TypeScript interface?

I want to make an object interface which force to use string as keys, but TypeScript compiler pass codes even if it had a number type as a key. Why?
For example,
interface PriceI {
[key:string]:number;
}
var coursePrice: PriceI = {};
coursePrice["Orange"] = 100;
coursePrice["Apple"] = 200;
coursePrice[3]=200; // It should be compile error, but it pass
coursePrice[true]=300; // It's compile error
See the handbook:
There are two types of supported index signatures: string and number. It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, JavaScript will actually convert that to a string before indexing into an object. That means that indexing with 100 (a number) is the same thing as indexing with "100" (a string), so the two need to be consistent.
In your example, TypeScript considers that coursePrice[3] = 200; is equivalent to coursePrice["3"] = 200;.

Categories

Resources