What does this typescript assignment do? - angular-ngrx-data

protected services: { [name: string]: EntityCollectionDataService<any> } = {};
I see it assigning an object literal to the services, but the [] assignment is unclear especially on the right side of the expression.

Related

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

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

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

Pipe transform use reduce for array of object

this is my Interface file :
export interface ListCount {
centre?: string;
cause?: string;
totalTime?: number;
}
I am trying to make reduce to array of objects with pipe transform :
export class SumPipe implements PipeTransform {
transform(items: ListCount[], attr: string): number {
return items.reduce((a, b) => a + b[attr], 0);
}
}
and in compent HTML I looking to make sum of totalTime
{{ (delayCount$ | async) | sum:'totalTime'}}
But I have this error :
error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ListCount'.
Then I change the param attr: string to attr: keyof ListCount
and still have this error :
error TS2365: Operator '+' cannot be applied to types 'number' and 'string | number'.
and
error TS2532: Object is possibly 'undefined'.
Any help please
I would suggest the following:
restrict attr to point to numeric properties
take into account that properties can be optional. Use zero for those.
alternatively filter out missing (undefined) values.
export interface ListCount {
centre?: string;
cause?: string;
totalTime?: number;
otherTime?: number;
}
type NumericKeys<T> = {
[P in keyof T]: T[P] extends number ? P : never;
}[keyof T];
type NumericListCountKey = NumericKeys<Required<ListCount>>;
class SumPipe {
transform(items: ListCount[], attr: NumericListCountKey): number {
const elemes = items.map(elem => elem[attr]);
return elemes.reduce((prev: number, currentVal) => prev + (currentVal ? currentVal : 0), 0);
}
}
// alternatively, with filter
class SumPipe2 {
static isNumber(n: number | undefined): n is number {
return n != null;
}
transform(items: ListCount[], attr: NumericListCountKey): number {
const elems = items.map(elem => elem[attr])
.filter(SumPipe2.isNumber);
return elems.reduce((prev: number, currentVal) => prev + currentVal, 0);
}
}
Playground link
Ok, so your code compiles without issues and works. Here is a working stackblitz. If it fails on your local environment, perhaps try checking TS version, try restarting the Angular CLI, look for other issues or at least provided information on which line exactly the error is thrown.
There are a few issues with your code though.
As others have noted, passing any attr to the pipe and using the bracket notation to get the property value makes no sense. Only object accepted by the pipe is ListCount[], so it HAVE to be totalTime that gets summed as it's the only numeric property. It would make sense if you made more generic pipe that would accept any[] though.
Your pipe is not guarded for your use-case. totalTime is an optional property, meaning that if any item doesn't have totalTime defined (which it can, according to your interface) the pipe will return NaN. Not sure if that's the desired behavior.
According to this article, reduce() has an optional index, which points to the current index in the array. Knowing this, I'd opt for the following solution, leaving out attr entirely.
return items.reduce((a, b, index) => a + b[index].totalTime, 0);

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)

TypeScript - Weak Types leads to Index Signatures, leading to poor type checking

This is more of a concern about the use of index signatures than AngularJs's use of it, but it is a key issue I believe needs resolving somehow with TypeScript types.
With TypeScript 2.4's Weak Types addition, the #types/angularjs IController started complaining about Weak Types, and the fix (the only possible fix at this point) is to add an index signature, as was referenced here:
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/17257
https://github.com/DefinitelyTyped/DefinitelyTyped/pull/17303
But BEFORE this change, TypeScript errors would help ensure that when you typed your IController with the properties in your object, it would complain (appropriately) that you missed defining properties, which is one of the very helpful things that makes TypeScript a great language (at least IMO).
Here's a direct example using a simplified AngularJs 1.5 Component:
let MyComponent = function(): angular.IComponentOptions {
return {
bindings: {},
controller: MyController,
controllerAs: 'vm',
}
}
interface IController extends angular.IController {
x: string;
}
function MyController(this: IController) {
let vm = this;
vm.x = "foo"; // OK
vm.x = 1; // ERROR: As expected due to the definition - great
// This next line would have complained before this,
// now it will let it thru unscathed, same with functions,
// arrays, etc. - this is the problem
vm.y = "bar"; // OK now, ERROR before
}
Is there a way to both allow AngularJs's types to avoid the Weak Types concern (which makes sense), while still allowing proper checking on child types?
Personally I feel that index signatures should be avoided wherever possible because of this issue, and are not great solutions for avoiding compiler errors (although currently likely the only way).
Thanks
The only solution I can come up with would require a change in your local angular type definition files (or maybe it could be pushed upstream). Right now the definition of IController in angular looks something like this:
interface IController {
$onInit?(): void;
$doCheck?(): void;
$onChanges?(onChangesObj: IOnChangesObject): void;
$onDestroy?(): void;
$postLink?(): void;
[s: string]: any;
}
Maybe it should be changed to this:
interface IControllerWeak {
$onInit?(): void;
$doCheck?(): void;
$onChanges?(onChangesObj: IOnChangesObject): void;
$onDestroy?(): void;
$postLink?(): void;
}
interface IController extends IControllerWeak {
[s: string]: any;
}
This should be exactly the same for everyone downstream, but it now gives you a reference to IControllerWeak, which is just IController without the index signature. In most (all?) cases, something which asks for an IController will accept an IControllerWeak and vice-versa.
So now you can extend IControllerWeak with a required property, and you have a non-weak (strong?) type with the guarantees you want:
interface IController extends angular.IControllerWeak {
x: string;
}
function MyController(this: IController) {
let vm = this;
vm.x = "foo"; // OK
vm.x = 1; // ERROR
vm.y = "bar"; // ERROR as desired
}
And an IComponentOptions.controller is happy enough to accept MyController (or a constructor for a subtype of IControllerWeak):
let MyComponent = function(): angular.IComponentOptions {
return {
bindings: {},
controller: MyController,
controllerAs: 'vm',
}
}
Does that work for you?
Can you solve it with mapped types, which lets you define a strong interface, but make its properties optional with the Partial mapped type...
// We don't want a weak type... or an index signature...
interface WeakType {
x?: string;
}
interface StrongType {
x: string;
}
type PartialStrongType = Partial<StrongType>;
function doSomething(options: PartialStrongType) {
return options.x || '';
}
doSomething({}); // OK
doSomething({ x: 'str' }); // OK
doSomething({ x: 1 }); // x can't be a number
doSomething({ y: 'str' }); // hey, you typed 'y', did you mean 'x'

Resources