I'm starting to learning typescript applied to node JS backend.
For now i'm still using function and not class.
I used to write named function for each file like
const item= {
a:1,
b:2,
function1:()=>{
console.log(item.a)
},
function2:()=>{
console.log(item.b)
} }
then export it and use like item.function1. (i sometimes use it also as function1 with import as unstructured object)
Now using typescript i'm still using this approach but with types. The problem is that i can't assign a type to a because it's seen as value. I can't heither do :
const item= {
function1:()=>{
item.a = 3
console.log(item.a)
},
function2:()=>{
item.b = 4
console.log(item.b)
}}
because it's saying that property a or b does not exist in type item.
another thing i tried but that doesn't work is:
const item = {
function1:()=>{
item.a:number = 3
console.log(item.a)
},
function2:()=>{
item.b:number = 4
console.log(item.b)
} }
Anyone can help me? hoping that this named functions are not a bad habit for write code
When you declare a variable and don't specify its type, typescript will try to infer what its type is. When you assign an object, it will try to infer the type like this: look at what properties you wrote and consider that the object only has these properties with exactly the types that the object you wrote has. So in your last code example typescript will infer that item is of type
const item: {
function1: () => void
function2: () => void
} = {
// ...
}
You can see that it didn't add any other properties.
Next, if you don't declare a property when typing an object, typescript will think that this property doesn't exist and may not exist on the object. Consider this:
const obj: { foo: number } = {
foo: 6
}
obj.bar = 7 // Error
You didn't declare the bar property, so typescript doesn't allow to read it or assign something to it. This is why you cannot write item.a = 3 in your example: typescript didn't infer that item object has property a and it thinks that it must not exist
To solve this you just need to either assign all properties you will need when creating your object:
const item = {
a: 1,
b: 2,
function1: () => { /* ... */ },
function2: () => { /* ... */ },
}
Or type item manually
interface Item {
a?: number
b?: number
function1: () => void
function2: () => void
}
const item: Item = {
function1: () => {
item.a = 3
},
function2: () => {
item.b = 4
}
}
Note the question marks before the column inside the interface, this is to tell that these properties are optional. If you don't set these question marks, typescript will think these are obligatory, so it will emit an error if you create item and don't declare a and b properties
It is possible to denote item object as any.
To accomplish a desired consistency an interface could be defined as follows
interface ItemObject {
[key: string]: any
}
var item: ItemObject= {};
to make compact:
var item: {[k: string]: any} = {};
now item can accept any string as key and any type as value
Related
I'm learning TypeScript, and decided to try implement it in a small portion of my codebase to get the ball rolling. Specifically, what I'm refactoring now is related to a fixture "factory" for the purpose of generating fixtures for Jest tests.
In addition to these factories, which spit out certain Objects, I also have some helper methods that make things like generating multiple objects a bit easier.
A factory is fairly simple, it looks something like this (the values are spoofed with faker.js):
function channelFactory(): ChannelItem {
return { foo: "bar" }
}
A ChannelItem is just a simple Object containing some keys
interface ChannelItem { foo: string; }
And as an example of one of those helper methods, I have a createMany function that takes in a Factory function and a Count as arguments
function createMany(factory: () => Record<string, unknown>, count = 1): Record<string, any>[] {
// A for loop that calls the factory, pushes those into an array and returns that array
}
However, if I try to use these factories somewhere, for example in this function that persists some created channels into the DB, I get the TS compiler warning me about Record<string, any>[] not being assignable to ChannelItem[].
function saveChannels(payload: ChannelItem[]): void { // Unimportant implementation details }
const items = createMany(channelFactory, 5);
saveChannels(items) // => Argument type Record<string, any>[] is not assignable to parameter type ChannelItem[] Type Record<string, any> is not assignable to type ChannelItem
I know this is a commonly known issue with Interfaces specifically (Issue #15300) and that the potential solution would be to declare a type rather than an interface, however in this situation I still get the same warning.
type ChannelItem = { foo: string } // Still gives me the above warning
What would the ideal way of making my factory functions more generic here be?
You could make the createMany function generic:
function createMany<K extends string, T>(factory: () => Record<K, T>, count = 1): Record<K, T>[] {
const arr = [];
for (let i = 0; i < count; i++) {
arr.push(factory());
}
return arr;
}
const items = createMany(channelFactory, 5);
console.log(items);
// Prints:
//[
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' }
//]
I made K extends string because you specified you want your record to have string keys. T can be anything you want.
Just have to fill in the functions yourself, not sure what you want done in those.
The createMany doesn't even need to know the type factory returns.
You can make it generic for more flexibility.
interface ChannelItem { foo: string; }
function channelFactory(): ChannelItem {
return { foo: "bar" }
}
function createMany<T>(factory: () => T, count = 1): T[] {
// A for loop that calls the factory, pushes those into an array and returns that array
return []
}
function saveChannels(payload: ChannelItem[]): void { }
const items = createMany(channelFactory, 5);
saveChannels(items)
TS playground
I have an Object like this:
const Foo = {
bar: (): void => { console.log('foo.bar') },
baz: (): void => { console.log('foo.baz') },
};
I would like to call these functions this way:
Foo["bar"]();
Foo["baz"]();
Unfortunately, TypeScript doesn't compile this code.
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type {MY_OBJECT}
What do I need to do to enable the function call by the index name?
#EDIT
I create this snippet to show the problem:
Playground Link
The code you provided works as-is, at least in typescript 3.9.2. It's possible though that either 1. it does not work that way in an earlier version of TS, or 2. the code you provided was simply an example and isn't exactly the code you have trouble with.
In either case, the error has to do with string and subtypes of string (bar and baz).
Your object Foo's type is inferred as:
{ [key in 'bar' | 'baz']: () => void }
as opposed to
{ [key in string]: () => void }
or in other words, the keys can only be specific sub-types of string.
When you run Foo["bar"]();, the latest typescript should correctly interpret the index provided as 'bar', but an earlier version might not be doing so.
You can get around this by forcing TS to interpret this as a literal type using a const assertion like so:
const key1 = 'bar' as const
Foo[key1]() // should not error
const key2 = 'baz' as const
Foo[key2]() // should not error
Alternatively, you can provide a type annotation to your Foo object with a wider type like so:
const Foo: {[key in string]: () => void} = {
bar: (): void => { console.log('foo.bar') },
baz: (): void => { console.log('foo.baz') },
};
Foo['bar']() // should not error
Foo['baz']() // should not error
How do I type the function's arguments bellow and keep them clean (without Typescript), using Interface?
// external file
export interface TSomeFunctionArgs {
someKey: string
// also here should be a type for a function
}
// main file
import { TSomeFunctionArgs } from "pathToFile"
const someFunction = (args: TSomeFunctionArgs) => {
// ... function's logic
}
someFunction({ someKey: "some string" }, someAnotherFunction)
In my example I'm passing two arguments, the first one is an object with the particular key - value pair, and a second one is a function (it could be any function).
How do I describe it in the Interface above?
In this case you can use generics for, and the code will be something like this:
interface TSomeFunctionArgs {
someKey: string
}
const someFunction = <U, T>(args: U, callback: T) => {
// ... function's logic
}
function someAnotherFunction() {}
// With generics you can give the type of the two parameters when you use it.
someFunction<TSomeFunctionArgs, () => void>({ someKey: "some string" }, someAnotherFunction)
Here is an object with several different key and value, and each props of value differ from each other, how to best describe this object using TypeScript? Especially the setValue method, how to limit type of the creatureType, prop and value?
const object = {
john: {
name: '',
age: 18
},
alien: {
height: 20,
power:100,
},
setValue(creatureType) {
const self = this
return function (prop) {
return function (value) {
self[creatureType][prop] = value
}
}
}
}
Your setValue() method will need to be generic if you want it to place strong restrictions on which properties and values go with which, uh, "creature type". Because the type of the object's setValue() method will be dependent on the type of the other properties of object, the compiler will give up trying to infer types for it; it's too circular for something that isn't a class. Either you could manually annotate all the types, which would be annoying, or you could split object into two pieces, say plainObject holding just the data, and then merge in the setValue() method which will be dependent on the type of plainObject, like this:
const plainObject = {
john: { name: '', age: 18 },
alien: { height: 20, power: 100 }
}
type PlainObject = typeof plainObject;
const object = {
...plainObject,
setValue<K extends keyof PlainObject>(creatureType: K) {
const self: PlainObject = this;
return function <P extends keyof PlainObject[K]>(prop: P) {
return function (value: PlainObject[K][P]) {
self[creatureType][prop] = value;
}
}
}
}
And you can verify that the compiler behaves as you want:
object.setValue("john")("age")(19); // okay
object.setValue("alien")("height")("pretty tall"); // error!
// "pretty tall" isn't numeric --> ~~~~~~~~~~~~~
object.setValue("john")("power")(9000); // error!
// "power" is wrong --> ~~~~~~~
object.setValue("elaine")("name")("elaine"); // error!
// "elaine"? -> ~~~~~~~~
Okay, hope that helps; good luck!
Link to code in Playground
Here's my code to test equality of some class objects. See my other question if you want to know why I'm not just doing
expect(receivedDeals).toEqual(expectedDeals) and other simpler assertions.
type DealCollection = { [key: number]: Deal }; // imported from another file
it("does the whole saga thing", async () => {
sagaStore.dispatch(startAction);
await sagaStore.waitFor(successAction.type);
const calledActionTypes: string[] = sagaStore
.getCalledActions()
.map(a => a.type);
expect(calledActionTypes).toEqual([startAction.type, successAction.type]);
const receivedDeals: DealCollection = sagaStore.getLatestCalledAction()
.deals;
Object.keys(receivedDeals).forEach((k: string) => {
const id = Number(k);
const deal = receivedDeals[id];
const expected: Deal = expectedDeals[id];
for (let key in expected) {
if (typeof expected[key] === "function") continue;
expect(expected[key]).toEqual(deal[key]);
}
});
});
The test passes fine, but I'm getting a Flow error on expected[key]:
Cannot get 'expected[key]' because an index signature declaring the expected key / value type is missing in 'Deal'
I can paste in code from Deal by request, but I think all you need to know is that I haven't declared an index signature (because I don't know how!).
I've searched around a bit but I can't find this exact case.
Update: I can eliminate the errors by changing deal and expected thusly:
const deal: Object = { ...receivedDeals[id] };
const expected: Object = { ...expectedDeals[id] };
And since I'm comparing properties in the loop this isn't really a problem. But I would think that I should be able to do this with Deals, and I'd like to know how I declare the index signature mentioned in the error.
PS. Bonus question: In some world where a mad scientist crossbred JS with Swift, I imagine you could do something like
const deal: Object = { ...receivedDeals[id] where (typeof receivedDeals[id] !== "function" };
const expected = // same thing
expect(deal).toEqual(expected);
// And then after some recombining of objects:
expect(receivedDeals).toEqual(expectedDeals);
Is this a thing at all?
Edit:
Adding a bit of the definition of Deal class:
Deal.js (summary)
export default class Deal {
obj: { [key: mixed]: mixed };
id: number;
name: string;
slug: string;
permalink: string;
headline: string;
// ...other property definitions
constructor(obj?: Object) {
if (!obj) return;
this.id = obj.id;
this.name = obj.name;
this.headline = obj.headline;
// ...etc
}
static fromApi(obj: Object): Deal {
const deal = new Deal();
deal.id = obj.id;
deal.name = obj.name;
deal.slug = obj.slug;
deal.permalink = obj.permalink;
// ...etc
return deal;
}
descriptionWithTextSize(size: number): string {
return this.descriptionWithStyle(`font-size:${size}`);
}
descriptionWithStyle(style: string): string {
return `<div style="${style}">${this.description}</div>`;
}
distanceFromLocation = (
location: Location,
unit: unitOfDistance = "mi"
): number => {
return distanceBetween(this.location, location);
};
distanceFrom = (otherDeal: Deal, unit: unitOfDistance = "mi"): number => {
return distanceBetween(this.location, otherDeal.location);
};
static toApi(deal: Deal): Object {
return { ...deal };
}
static collectionFromArray(array: Object[]) {
const deals: DealCollection = {};
array.forEach(p => (deals[p.id] = Deal.fromApi(p)));
return deals;
}
}
An index signature (or indexer property) is defined as [keyName: KeyType]: ValueType. DealCollection is a great example: the keyName is key, the KeyType is number and the ValueType is Deal. This means that whenever you access a number property of an object of type DealCollection, it will return a Deal. You will want to add a similar expression to the definition of Deal in order to access arbitrary properties on it. More information can be found at the Objects as maps section in the Flow documentation.