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

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'

Related

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);

How to write mobx action methods in separate files and import them to the actual mobx class having observable variables?

I have a mobx class which has an observable variable named dataObject. This observable variable is an object and its structure is:
{
name:string;
dataType:string;
value: string;
description:string;
.
.
.
.
.
..... total around 45 such properties
}
Each object property has an action method to update it. For example
#action updateName = (name:string) =>{
this.dataObject.name = name;
}
It is difficult to maintain all such 45 action methods in same class. Is there any way to write these methods in a separate ts file and import them to the current file?
EDIT: Adding a part of dataObject interface along with few of its action methods
interface IDataObject{
name:string;
dataType:string;
value:any;
valueType:VALUE_TYPE|any;
hasCustomValueType:boolean;
customValueType:string|undefined;
description:string;
isRegistered:boolean;
associatedVariableDetails:IVariableDetails[];
hasMultipleValues:boolean;
associatedService:SERVICE;
}
enum VALUE_TYPE{
MAJOR = 'major',
MED = 'med',
MINOR = 'minor'
}
enum SERVICE{
PUSH_IN = 'pushin',
PULL_OUT = 'pullout'
}
interface IVariableDetails{
variableName:string;
varirbleDataType:string;
variableValue:any;
hasArray:boolean;
isDeletable:boolean;
}
//////////////
method to update dataType
#action updateDataType = (dataType:string) =>{
this.dataObject.dataType = dataType;
if(dataType === 'string'){
this.dataObject.value = ''
}
else if(dataType === 'boolean'){
this.dataObject.value = false
}
else if(dataType === 'integer'){
this.dataObject.value = 0
}
}
methods to modify associatedVariableDetails
#action addVariableDetails = (variableDetails:IVariableDetails) =>{
this.dataObject.associatedVariableDetails.push(variableDetails);
}
#action updateMultipleValueState = (hasMultipleValues:boolean) =>{
this.dataObject.hasMultipleValues = hasMultipleValues;
if(!hasMultipleValues){
this.dataObject.associatedVariableDetails = this.dataObject.associatedVariableDetails[0];
}
}
Create an interface for the object that you want as the observable and add an index signature.
This looks something like this:
interface foo {
[key: string]: type;
bar: string;
}
Then pass the key to your update action and update the value like object[key] = newValue.
Example:
#action update = (key: string, value: type) => {
object[key] = value;
}
This will allow you to use one single function to patch any value into your observable object.
This is a good article that explains index signatures in depth: https://basarat.gitbooks.io/typescript/docs/types/index-signatures.html
To answer your original question though. You could create 45 functions and export all of them in an object and import them into this file, but that solution may be overkill and tedious to maintain if types or fields change. I can show an example of that if this solution does not work. The solution I have explained above is the more efficient way of handling object patches.
EDIT:
I apologize for the delay in editing my answer. I have made edits to your code above showing the approach I would be taking with this kind of object you have. Here is the edit:
interface IDataObject{
[key: string]: string | boolean | VALUE_TYPE | SERVICE | IVariableDetails[];
name: string;
dataType: string;
value: any; // THIS SHOULD BE STRICTLY DEFINED
valueType: VALUE_TYPE|any; // THIS SHOULD ALSO BE STRICTLY DEFINED
hasCustomValueType: boolean;
customValueType?: string;
description: string;
isRegistered:boolean;
associatedVariableDetails: IVariableDetails[];
hasMultipleValues: boolean;
associatedService: SERVICE;
}
enum VALUE_TYPE{
MAJOR = 'major',
MED = 'med',
MINOR = 'minor'
}
enum SERVICE{
PUSH_IN = 'pushin',
PULL_OUT = 'pullout'
}
interface IVariableDetails{
variableName:string;
varirbleDataType:string;
variableValue:any;
hasArray:boolean;
isDeletable:boolean;
}
#action updateField = (key: string, value: string | boolean | VALUE_TYPE | SERVICE) => {
this.dataObject[key] = value;
}
#action addVariableDetails = (variableDetails: IVariableDetails) => {
this.dataObject.associatedVariableDetails.push(variableDetails);
}
#action updateMultipleValueState = (hasMultipleValues: boolean) => {
this.dataObject.hasMultipleValues = hasMultipleValues;
if(!hasMultipleValues){
this.dataObject.associatedVariableDetails = this.dataObject.associatedVariableDetails[0];
}
}
What I have done here is exposed a single method to manipulate the semi primitive values that do not need extra work after, they just want new values. Then I would create more complex #actions as needed based on the type of work you need to do on your object. As shown the updateField action does not allow you to use it with a VariableDetails type of parameter which will make it so typescript complains if you try to send a variableDetails parameter to that action.
I have also noted that the any attributes should definitely be some type if the rest of the object has strict typings. If you are going to have any types in your object you might as well make an interface like interface IAny { [key: string]: any; } and use that everywhere.
I hope this can be of help, and again I apologize for the gap in responses.

TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature

I have a little trouble implementing the cropperjs in my project. I keep getting the above error even though I believe it is injected right.
have a look at my code please
module view.pages.controllers {
export class CropToolController {
public static $inject = ["Cropper"];
constructor(
private Cropper: cropperjs.Cropper
) {
var image = document.getElementById('image');
var cropper = new Cropper(image, {
aspectRatio: 16 / 9,
crop: function(e) {
console.log(e.detail.x);
console.log(e.detail.y);
console.log(e.detail.width);
console.log(e.detail.height);
}
});
}
private getFileUrl(id: string) {
return this.$urlHelper.getAssetUrl(id);
}
}}
Don't know if there is an error in definition file but it seems good to me
export class Cropper {
constructor(element: HTMLImageElement, options: CropperOptions);
crop(): void;
reset(): void;
...}
Anyone has an idea what am I doing wrong here? Thanks
You trying to inject "Cropper", but you need an Angular module to inject it. Did you try Ng-Cropper?

Index signature is missing in Type - AngularJS + TypeScript validation using $validators

I'm trying to create a directive using AngularJS 1.5 and TypeScript 1.7 so I can do some custom form validation.
I've followed this example but Typescript gives me a 'Type signature is missing in type' when I extend the ng.INgModelController
interface IOneItemRequiredValidator extends ng.INgModelController {
$validators: {
oneItemRequired(modelValue: any, viewValue: any) : boolean;
};
}
I've checked $validators and it's of type IModelValidators, with the below signature:
interface IModelValidators {
[index: string]: (modelValue: any, viewValue: any) => boolean;
}
so I tried to follow that and now TypeScript is not complaning, but when I try to access the new property of the $validators object it can't find it.
interface IOneItemRequiredValidator extends ng.INgModelController {
$validators: {
[oneItemRequired: string]: (modelValue: any, viewValue: any) => boolean;
};
}
angular
.module('app')
.directive('oneItemRequired', () => {
return {
restrict: 'A',
require: 'ngModel',
link: (scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller: IOneItemRequiredValidator) => {
controller.$validators.oneItemRequired = (modelValue, viewValue) => {
return viewValue !== null && viewValue !== undefined && viewValue.length > 0;
}
}
};
});
Am I wrongly declaring my new IOneItemRequiredValidator interface? Thanks!
I don't use Angular 1 with TypeScript but generally speaking, in TypeScript, the name you give to these doesn't make a difference at all. You are just saying that you accept any extra property not named in the interface.
This means that the actual Angular declaration file is the one that should have added this bit in IModelValidators declaration. See here for details.
The problem with the old way is that the type of the object with a single function oneItemRequired is not compatible with the IModelValidators type.
You can fix that in one of two ways:
(1) Just put back the indexer, like:
interface IOneItemRequiredValidator extends INgModelController {
$validators: {
oneItemRequired(modelValue : any, viewValue : any) : boolean;
[otherProps: string]: (modelValue: any, viewValue: any) => boolean;
};
}
A simple non-Angular example
(2) Extend the interface manually, like:
interface IOneItemRequiredNgModelValidators
extends ng.IModelValidators {
oneItemRequired(modelValue : any, viewValue : any) : boolean;
}
interface IOneItemRequiredValidator extends ng.INgModelController {
$validators: IOneItemRequiredNgModelValidators
}
A simple non-Angular example
This first one is less code that only aims to serve the tool really, except it's a little hacky. The 2nd one feels cool, but it's more just for the tool. So, I'll let you pick :)
Let me know if they work for you.
Cheers,

Can I add array accessors to generic TypeScript classes?

I have a list class that looks like this:
class List<T> {
private _array: Array<T>;
constructor() {
this._array = new Array<T>();
}
get count() { return this._array.length; }
public add = (state) => {
this._array.push(state);
}
...
}
And I would like to access the internal array from the class:
var something = list[0];
In c# I would do it something like this:
public T this[int index]
{
get
{
return _array[index];
}
private set {}
}
}
But I can't see anyway to accomplish this in TypeScript. Is there a way to add array accessors to my class so it looks more like a generic List ?
Thanks for the brainpower!
You can though the syntax is a bit weird. Note that since typescript gets compiled to js, only numbers and strings are valid keys:
interface IList<T> {
[index: number]: T
}
interface IMap<T> {
[index: string]: T
}
interface IMap<K, V> {
[index: K]: V // Error: Index signature parameter type must be either string or number
}
There's a trick to it though. You can't actually overload the operator, you can only tell the compiler that it exists. For instance if you have a generic object that you wish to use as a hashtable - declare it as Map<T> instead of any. The same goes for arrays.
The only possible way to actually put the operator in good use is to use an array or object as the underlying element, declare them as IList/IMap and then fiddle with their properties / prototype to add specific functionality. For example, to create an observable array see this answer

Resources