Types of property 'type' are incompatible. Problem with Typescript union types - reactjs

Background
I am building a react site with some reusable generic UI components. Our backend service will return some responses with the data conforming to an abstract type.
For example
interface TypeAServerResponse {
somefield: string,
otherfield: string,
}
interface TypeBServerResponse {
somefield: string,
}
type TypeServerResponseUnion = TypeAServerResponse | TypeBServerResponse;
Both of the server response types contain somefield, and we would like to display that in the reused UI component. So we union them and tell the component to expect TypeServerResponseUnion.
However, in some occasions, we would also want to use otherfield, so we need to tell TypeScript to we are discriminating the union type. Without changing the backend to return a string literal, we are extending the ServerResponse types to contain a type string literal.
interface TypeA extends TypeAServerResponse{
$type: 'a',
}
interface TypeB extends TypeBServerResponse{
$type: 'b',
}
type TypeUnion = TypeA | TypeB; //or
type TypeUnion = TypeServerResponseUnion extends {
$type: 'a'|'b',
}
Now we can check on $type field in our UI component to discriminate the union and get otherfield when possible.
The problem
We now have some method to fetch the data from the server that returns TypeServerResponseUnion, and we want to parse it to TypeUnion before providing it to the UI layer.
// could be ajax.get, could be axios
const serverGet = () : TypeServerResponseUnion => {
return {somefield: 'something'}
}
const parse = () : TypeUnion => {
const response : TypeServerResponseUnion = serverGet();
// do something here to add the $item field and return it
}
We have two use cases
We know which concrete type we are asking for, so we can just provide the $type to the function. This has some problems I don't know how to deal with.
We don't know which concrete type we are asking for, we only know we are asking for a same type as we already have, this is the part where I am struggling with.
So I have the parse function as such:
const get = (original: TypeUnion) => {
const response = serverGet();
const parsedResponse: TypeUnion = {...response, $type: original.$type}
return parsedResponse
}
It complains with error:
Type '{ $type: "a" | "b"; somefield: string; otherfield: string; } | { $type: "a" | "b"; somefield: string; }' is not assignable to type 'TypeUnion'.
Type '{ $type: "a" | "b"; somefield: string; }' is not assignable to type 'TypeUnion'.
Type '{ $type: "a" | "b"; somefield: string; }' is not assignable to type 'TypeB'.
Types of property '$type' are incompatible.
Type '"a" | "b"' is not assignable to type '"b"'.
Type '"a"' is not assignable to type '"b"'.(2322)
So I want to know what is the best way to do typing for those types to solve this use case we are facing.
Extension
I also want to discuss this related problem with typescript.
If I change the get function to the following:
const get = (original: TypeUnion) => {
const response = serverGet();
const parsedResponse: TypeUnion = {} as TypeUnion;
parsedResponse.$type = original.$type
return parsedResponse
The error goes away, but of course because we are doing wrong type casting so it is not safe.
The question is why can we assign original.$type to TypeUnion.$type, where previously
const parsedResponse: TypeUnion = {...response, $type: original.$type}
we are assigning original.$type to $type during construction time does not work.
Playground with code

The reason for the type error is because you do not know if original and response are of the same type. Since they are both unions one could be of type "A" and the other of type "B". This might be be the true in the real implementation but it's not true in terms of the types.
We know which concrete type we are asking for, so we can just provide the $type to the function. This has some problems I don't know how to deal with.
This sorta seems like an anti pattern to me. If you know what type is coming from the backend then serverGet should not return a union but have the return type TypeAServerResponse or TypeBServerResponse
We don't know which concrete type we are asking for, we only know we are asking for a same type as we already have, this is the part where I am struggling with.
For this it is probably best to write a helper parse function (as you already did) but have but parse based on the data and not a value that is passed in.
const getPraseValueFromSErver: ()=>TypeUnion= () => {
const res = serverGet();
if("otherfield" in res){
// we know we have type A since otherfield exsists in the data
return { $itemType: "a", ...res}
} else {
// we know we have type B
return { $itemType: "b", ...res}
}
}
See full playground here

Related

Typescript error when using array.map in a function

interface Object1{
id:number,
label:string
}
interface Object2{
id:number,
description:string
}
const transformArray:Object2[] = (input:Object1[])=>{
return input.map(item=>{
return {
id:item.id,
description:item.label
}
})
}
In this code typescript gives the error
Type '(input: Object1[]) => { id: number; description: string; }[]' is
missing the following properties from type 'Object2[]': pop, push,
concat, join, and 28 more.ts(2740) index.tsx(44, 34): Did you mean to
call this expression?
I'm trying to create a function that takes an array of Object1 and converts to array of Object2
This:
const transformArray:Object2[] = (input:Object1[])=>{
means that you are saying transformArray is of type Object2[]. And then you assigning a function to it. An array and a function are not compatible types, so you get a type error.
I think you meant to do this:
const transformArray = (input:Object1[]): Object2[] => {
Here you declare a function that accepts an array of Object1s and returns an array of Object2s. transformArray does not require an explicit type because the function your are assigning to it very strongly typed and Typescript can figure out the tight type from that.
Playground

Conditional Type Checking based on function parameter

Im writing a custom Hook that can take either one or two strings, or an object with more granular parameters. I wanted to add a conditional type check to check for a type of that param and do some logic based on it. Here are the relevant snippets:
// The hook itself
const [error, loading, data] = useFirestore('posts', 'test'); // in a string version
const [error, loading, data] = useFirestore({...someProps}); // in a object version version
// The types that i defined for them
type queryType<T> = T extends string ? string : documentQueryType;
type docType<T> = T extends string ? string : never;
type documentQueryType = {
collection: string;
query: string[] | string[][];
limit: number;
orderBy: string; // todo limit this to be only special words
order: string; // todo same as above
startAt: number;
endAt: number;
};
// The function that is in the question
export const useFirestore = <T>(query: queryType<T>, doc?: docType<T>) => {...rest of the function
How would I make the last snippet work so when passed an object it sets doc to never, and when passed a string sets the doc to string?
This can be partially achieved with conditional types, but it may not be 100% type-safe. Since doc is optional, it will not be required when the query is a string, and will still allow undefined when query is an object.
However, if these two scenarios are not an issue, this can be achieved with conditional types:
// Simplified type
type DocumentQueryType = {
collection: string;
};
// The two types of queries that are accepted
type QueryTypes = string | DocumentQueryType;
// Given the query type, infer the doc type
type InferDocType<QueryType> = QueryType extends string ? string : never;
const useFirestore = <QueryType extends QueryTypes>(query: QueryType, doc?: InferDocType<QueryType>) => { }
// Valid Examples
useFirestore('posts', 'test');
useFirestore({ collection: "" });
// Valid Examples (may not want these?)
useFirestore('posts');
useFirestore({ collection: "" }, undefined);
// Invalid Examples
useFirestore({ collection: "" }, "test");
// Argument of type '"test"' is not assignable to parameter of type 'undefined'.(2345)
useFirestore('posts', null);
// Argument of type 'null' is not assignable to parameter of type 'string | undefined'.(2345)

how to narrow the type for SVG Element union

I am using react to set a reference to an svg element that might be a <rect>, <polygon> or <ellipse>.
I have this declaration:
const shapeRef = useRef<SVGPolygonElement | SVGEllipseElement | SVGRectElement>(null);
But when I try and set this on an <ellipse> element like this:
<ellipse
cx={width / 8}
cy={-sideDimension(y) / 8}
rx={width}
ry={height}
ref={shapeRef}
/>
I get this error:
Type 'RefObject' is not assignable to type 'string | ((instance:
SVGEllipseElement | null) => void) | RefObject |
null | undefined'. Type 'RefObject' is not assignable to type
'RefObject'.
Type 'SVGPolygonElement | SVGEllipseElement | SVGRectElement' is not assignable to type 'SVGEllipseElement'.
Type 'SVGPolygonElement' is missing the following properties from type 'SVGEllipseElement': cx, cy, rx, ryts(2322)
My understanding from this is that I somehow need to narrow the type in order for this to work or else every object that uses this ref must have all properties of the union.
You are correct. Typescript gives you that error because it doesn't know which one of the types it should account the shapreRef as.
The best solution IMO is using a Type Guards. A Type Guard is the typescript way to check if a variable is of a certain type. For union types, that gives typescript the understanding that something is of a specific type.
For example, in your case, it can be something like this:
interface IEllipse {
attr1: string;
attr2: string;
}
interface IRect {
attr3: string;
attr4: string;
}
type SvgShape = IEllipse | IRect | IPolygon;
function isEllipse(shape: SvgShape): shape is IEllipse {
return (shape as IEllipse).attr1 !== undefined;
}
Notice that the return type is shape is IEllipse. This means that typescript will interpret a truthy return value here as if shape is an IEllipse.
Then, wherever you want to use a SvgShape, you can check which type of SvgShape it is and typescript should know the type based on that:
// ...
render() {
const shape: SvgShape = this.getCurrentShape();
if (isEllipse(shape)) {
// typescript should KNOW that this is an ellipse inside this if
// it will accept all of Ellipse's attribute and reject other attributes
// that appear in other shapes
return <ellipse .../>;
} else if (isRect(shape)) {
// typescript should interpet this shape as a Rect inside the `if`
return <rect ... />;
} else {
// typescript will know only one subtype left (IPolygon)
return <polygon points="..." />;
}
}
// ...
Why not just an Intersection type?
Well... Intersection types are more for cases where every one of the types (Rect, Polygon, etc) have the exact same attributes in the new item.
For example:
type Inter = IRect & IPolygon & IEllipse;
Means that an Inter type is IRect and IPolygon and IEllipse. That means an object of this type will have all members of all three types.
So, trying to access the attribute points (which exists on IPolygon) on a shape that is actually an IRect, will act as if that attribute exists there (which we don't want)
You will mostly see intersection types used for mixins and other concepts that don’t fit in the classic object-oriented mold.
how to use with useRef?
type SvgShape = SVGPolygonElement | SVGEllipseElement | SVGRectElement;
const shapeRef = useRef<SvgShape>(null);
function isEllipseRef(shapeRef: MutableRefObject<SvgShape>): shapeRef is MutableRefObject<IEllipse> {
const shape: SvgShape = shapeRef.current;
return (shape as IEllipse).attr1 !== undefined;
}

Typescript Access objects nested within other types

I am relatively new to typescript and it's ins and outs and I am having trouble with a certain structure. it looks like this:
I have a function I am using to feed data to a timeline component. the data recieved by the function can be of type 1 or type 2. Now this is where it gets complicated type 1 is a an object. However, type 2 can be any 1 of 4 different types
type1 : {}
type2 : type3 | type4 | type5 | type6
types 3-6 differ slightly in structure from one another and cannot be combined. In the function below the isConversion flag is a check for an (object: type6). This object has another object inside it of type 7.
type 6: {
...,
type7: {
...,
conversions
}
}
inside type7 is a field called conversions that has the data I need to pass to the timeline.
timelineItems = (items: type1 | type2): PropsData => {
const { dataType, isConversion } = this.state
if(isConversion){
const {comments, type7.conversions } = items as type6
return {
comments
type7.conversions
}
}
I have a work around where I fetch type7 when I get the data and set it to the state. and use that value but I would like to to know if there is a way to get the conversions object as above.
Thank You.
So you want to know if you can determine if items type is Type6?
Typescript types are lost after compilation so you have to write your own typeguard which will return true if object is Type6. It's pure javascript check, for example if you know that only Type6 has certain field you can check if it is present in this object.
interface Type6 {
comments: any[];
type6: Type7;
}
interface Type7 {
conversions: any[];
}
function isType6(obj): obj is Type6 {
return obj.type6 !== undefined;
}
Then you can use this typeguard like this:
if(isType6(items)) {
const {comments, type6: {conversions}} = items; // no cast needed, ts know it is Type6
return {comments, conversions };
}

Union Types expecting other type when passed as props to component?

I'm using Typescript with React.
I am retrieving data from an API that returns two type: VirtualMachine or Disk. The backend takes responsibility for distinguishing the resource type and returns the type of both depending on the results of the query:
requestMoreInfo: (resourceType: string, resourceId: number): AppThunkAction<ResourceActions> => (dispatch, getState) => {
let fetchResourceInfo = fetch('http://localhost:5004/GetResourceTypeInformation/' + resourceType + '/' + resourceId, {
method: 'GET'
})
I've declared a union type for my Redux state:
export interface ResourceState {
currentResourceInformation?: VirtualMachineInformation | DiskInformation;
}
and I am subsequently converting the response to the type determined by the resource type passed into the function and dispatching an action to update my components state. THIS IS WHERE I THINK I'M GOING WRONG.
if (resourceType == "Virtual Machine") {
var vmResponse = response.json() as VirtualMachineInformation;
dispatch({
type: 'RECEIVE_RESOURCE_INFO',
resourceInfo: vmResponse
});
}
else if (resourceType == "Disk") {
var diskResponse = response.json() as DiskInformation;
dispatch({
type: 'RECEIVE_RESOURCE_INFO',
resourceInfo: diskResponse
});
}
TypeScript appears to be happy with this. However, I am then trying to render a child component and passing this update state as a prop:
private requestResourceInformation = (resourceType: string, resourceId: number) => {
this.props.requestMoreInfo(resourceType, resourceId);
if (resourceType == "Virtual Machine") {
return <VirtualMachineResource virtualMachine={this.props.currentResourceInformation} />
}
}
This just maps a table with the data.
However, I'm retrieving the error:
Type 'VirtualMachineInformation | DiskInformation | undefined' is not assignable to type 'VirtualMachineInformation | undefined'.
Type 'DiskInformation' is not assignable to type 'VirtualMachineInformation | undefined'.
Type 'DiskInformation' is not assignable to type 'VirtualMachineInformation'.
Property 'azureVmId' is missing in type 'DiskInformation
I believe this is because TypeScript still considers the value as the union type and the expected value is present in VirtualMachine type but no present in the Disk type.
Where am I going wrong with this? Is there an explicit way to declare the specific type of the union after retrieving the data?
The virtualMachine property doesn't accept the DiskInformation interface as a value - and that is your problem. TypeScript compiler doesn't know what's the exact type of the value at the compile time so the type is guessed to be one among those three: VirtualMachineInformation, DiskInformation, undefined
As I wrote in the comments section - you can use (at least) three solutions to solve your problem:
use type assertions - https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions - you can not use <Type>value syntax in tsx files
return <SomeComponent prop={value as Type}></SomeComponent>
use type guards https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
if ([check if type of the value is the Type]) {
return [something else]
}
[TypeScript knows that the value IS NOT an instance of the Type here]
return <SomeComponent prop={value}></SomeComponent>
use overloads - http://www.typescriptlang.org/docs/handbook/functions.html#overloads
class X {
private y(x: "abc"): "cda";
private y(x: "cda"): "abc";
private y(x: string): string {
[method logic]
}
}

Resources