I have a TS definition file for Ext JS containing function store.add(a : any) (it has many overloads so I guess this is to simplify the d.ts file). I want to pass it a literal object which implements a Person interface:
interface Person
{
name: string;
age: number
}
store.add(<Person>{ name: "Sam" });
This gives me intellisense but unfortunately it is just coercing the literal into a Person, without detecting the missing field. This works as I want:
var p : Person = { name: "Sam" }; // detects a missing field
store.add(p);
But is there a way to do this without a separate variable?
I realise this would be solved with 'fixed' definition files, but I think many Javascript libraries have too many overloads to allow this. I almost need a way to dynamically overload the function definition..! Would generics help here?
Yes generics seem to be the answer. In the definition file changing:
add?( model:any ): Ext.data.IModel[];
to
add?<T>( model:T ): Ext.data.IModel[];
Allows you to call
store.add<Person>({ name: "sam" });
And it correctly shows an error!
Related
So, we have an app with multiple resources, let's say we have Product, Cart, Whatever resources. For each of those resources you can create activities, the main idea here is that for each resource there is an endpoint to create/update those activities, which looks the same no matter the resource you are trying to update.
So in our app (React) we created a single form to create/update an activity, it looks the same no matter for which resource you want to create an activity for, same fields, same possible values. Therefore we have one single component instead of 3, and a common function that handles the api part.
Something like:
const { mutate } = useUniversalEditActivity(variant); // variant can be 'product', 'cart', 'whatever'
We call mutate when we want to submit the form.
Inside that hook, there is a simple map:
const variantMapper = {
product: {
serviceAction: updateProductActivity, // simple function that wraps fetch, does the network request
},
cart: {
serviceAction: updateCartActivity,
},
whatever: {
serviceAction: updateWhateverActivity,
},
};
// Using it like
const mutatingServiceAction = variantMapper[variant].serviceAction;
...
await mutatingServiceAction();
The body is typed as
type UniversalEditActivityBodyType =
| UpdateProductActivityRequestBody
| UpdateCartActivityRequestBody
| UpdateWhateverActivityRequestBody
Which works when all the properties are the same across the types, but the problem starts now when the BE changed the spec for the Whatever resource.
So, before the request body had a property which had 2 possible values, so it was typed like:
type UpdateProductActivityRequestBody = {
propertyWithIssues: 'a'| 'b';
}
All 3 looked the same, but the spec changed for the Whatever resource to:
type UpdateWhateverActivityRequestBody = {
propertyWithIssues: 'a'| 'b' | 'c' | 'd';
}
Adding 2 more possible values for the same property, now there is a difference on how they look and inside my generic function that handled all body types, now I get the Type '"a" | "b" | "c" | "d"' is not assignable to type '"a" | "b"'.
I kind of understand the error, but not sure how to fix it in order for my function to still work with all those 3 types when just the possible values on a single property is different between them.
I don't know if I explained as good as I should have, but it's a more complex question (I think), so I tried my best. Please also suggest a different title if you think it would better describe my problem. Thanks in advance.
UPDATE 1:
#chris-hamilton, the request is executed like this:
const resp = await mutatingServiceAction(id, activityId, payload);
This is where the issue happens, because payload is a union of all those 3 types, but now they have become incompatible.
Minimum reproducible example here: https://codesandbox.io/s/gallant-bessie-sxjc4x?file=/src/index.ts
I know in theory that the issue could be solved by doing something like:
if (variant === 'product') {
// directly use product function
}
...
But I have the feeling this can be something different, as they have the exact same structure, just one property can have different values.
This error is really just a big red flag for this design pattern. You've specified that these two parameters can be one of many types, meaning you can have any combination of variant and body. ie. this function would accept a variant of "product" and a body of UpdateCartActivityRequestBody. But clearly that is not how the function is meant to be used.
It's a question of what these updateActivity functions are doing, and how similar they are. It may be that the parameters of these functions can accept a common type:
type Body = {
propertyWithIssues: string;
}
function main(
variant: "product" | "cart" | "whatever",
body: Body
) {
const serviceAction = variantMapper[variant].serviceAction;
serviceAction(body); // no error
}
So you need to ask yourself, "do my functions need to know about all these specific properties?".
If the answer is no, then define a type with only the properties needed for those functions. If those properties are common for all functions then the design pattern is fine. The type may just be Object, it depends what the functions are doing.
If the answer is yes, then the design pattern is incorrect, and you shouldn't be coupling these service actions into a single function. You should just be calling updateWhateverActivity directly, with the correct parameter type. No need for this variantMapper object.
Maybe you have other reasons for implementing this pattern, but you'll need to give more details if that's the case.
I can see it maybe being the case that you have an object with variant and body and you don't know what they are until run time. In that case you will have to do type narrowing like you showed. But you should also be checking that the type of body actually matches variant or you're just asking for runtime errors. This is really what the error is trying to tell you.
if (variant === 'product') {
// somehow verify that body is of type UpdateProductActivityRequestBody
// directly use product function
}
I think for my scenario there must be a better way to avoid duplicate code, but I can't get it to work..
I have an object like this:
const sectionTypes = {
foo: ...,
bar: ...
} as const;
Based on the object keys, I need to create an array of objects. But only a type array, not a real one.
something like this:
type SectionArray = [
{ type: 'foo', ... },
{ type: 'bar', ... }
]
currently I am creating the array manual, as I have not found a way to create an array dynamically based on the keys. Is there any way to avoid the repetitions? Thanks!
Playground Link with a more detailed recreation: Link
type SectionKeys = typeof sectionTypes
type Section = {type: keyof SectionKeys}
const array: Section[] = [{type: "foo"}]
This way, by annotating the Section[] type, you will be forced by typescript to provide an object with a property type that accepts only the keys of sectionTypes
Providing more props won't cause any typescript error, but there is also no restriction on it. But you got the idea. You can expand your type in whatever way you want.
interface Data {
petname: string,
petprice: string,
category: string,
pettype: string,
gender: string,
short: string,
details: string
}
const [petdata, setPetdata] = useState<Data>({});
const [img, setImg] = useState<string>('');
Error:
Argument of type '{}' is not assignable to parameter of type 'Data | (() => Data)'.
Type '{}' is not assignable to type '() => Data'.
Type '{}' provides no match for the signature '(): Data'
Short answer: the empty object you have provided as the default value upon initialisation, {}, does not match the Data interface as it is missing all the required properties in it. This is a good sign because that means TypeScript is doing its intended job: ensuring you don't assign objects that does not match the given interface.
Long answer: A solution will be to allow the state to be Data or null, so you can just instantiate it with null at the start until you have populated it later:
const [petdata, setPetdata] = useState<Data | null>(null);
Based on how your downstream code looks like, this change may lead to errors if you attempt to access properties inside the petdata object, e.g. petdata.petname, without first checking if petdata is nullish. This can be circumvented if you:
use the optional chaining operator when accessing sub properties, e.g. petdata?.petname, or
have a type guard before accessing any properties, e.g.
if (petdata) {
petadata.petname = ...;
}
Of course, a dirty escape hatch is to make all properties optional, i.e.:
const [petdata, setPetdata] = useState<Partial<Data>>({});
...but I would not recommend this for your case as that means you are no longer strict enforcing required properties on petdata, and these properties are likely to be compulsory/required by design.
Unlike in Java for example, Javascript does not really maintain classes at runtime. The TypeScript compiler does. Javascript maintains only "objects" with their properties and values (and methods of course, but in this case you only have properties). Those you defined are all obligatory, none of them are optional.
You can mark a property as optional by adding a ? behind the variable's name, like this:
details?: string
So the compiler tries to match your parameter {} to an interface you defined, but nothing matches, because Data would need all those obligatory properties to match. The method needs either an object of type Data or a method, that returns a Data object.
So you've got two options: you define Data's properties as optional, and therefore let {} match Data or pass all the data like this:
{
petname: '',
petprice: '',
category: '',
pettype: '',
gender: '',
short: '',
details: ''
}
Declaring your state as follows will accepts empty objects
const [petdata, setPetdata] = useState<Data>({} as Data);
I have a React state which is array of objects and I am adding an object to it in setState method. The objects adds correctly.
Now when I try to access the object in some other function, I can't access the property of object. I can see on console that the object exists with the properties. I checked other similar questions on stackoverflow which are related to asynchronous calls but mine does not fall under that.
I have created array of object like this,
interface ResultState{
....
localArray : object[];
}
Initializing it to null in constructor.
And adding an object to it like,
localArray : this.state.localArray.concat(Items1)
My console output :
localArray -> [{}]
0:
name: 'abcd'
roll_num: 10
address: [{...}]
0: {Street: 'abcdefgh', aptnum: 1}
Now I want to access address of the object.
My code is like this,
const resultObject = this.state.localArray[0];
return resultObject.address;
But I get an error property address does not exist on type object.
when I do console.log(typeof(resultObject)), comes object.
what could possibly the reason for this?
I'm glad you got it working, but using type any[] isn't great because it says that your array could contain anything. What you really want is for typescript to know the type of the specific objects in your array. Based on what you've posted, it should be something like:
interface Address {
Street: string;
aptnum: number;
}
interface Entry {
name: string;
roll_num: number;
address: Address[];
}
interface ResultState {
....
localArray: Entry[];
}
That way typescript knows that your array elements are objects, but it also knows what properties are valid and what the types are for each property.
Yes, it was because of the type of the array. After changing it to localArray: any[], it worked!
I am using flow, and mostly things are working out, but I am catching this error in my linting on something as trivial as this:
type Props = {
actions: object,
products: object,
};
type State = {
email: string,
name: string
};
class SomeComponent extends Component {
// etc..
}
The linting errors show right after the "type" keyword and the error is:
"Expecting newline or semicolon"
There are 2 possibilities that I see here:
1) object should be capitalized (Object)
2) You are not using eslint-plugin-flowtype
This may seem silly, but I had to go into IntelliJ IDEA -> Preferences -> Languages & Frameworks -> JavaScript and change JavaScript language version to Flow (was previously React JSX). I was already using eslint-plugin-flowtype, and was wondering why it was not working.