Where a function is required, typescript allows me to pass an object with an incompatible `apply` property - typescript-typings

Currently using typescript 3.4.5 with strict mode enabled...
Backstory
I just ran into a situation where typescript failed to protect me from my own mistakes, unfortunately. And I'm trying to figure out why typescript failed to catch this error.
I was writing a type declaration for a function like this:
function acceptVisitors (visitor) {
visitor.apply(value);
}
Astute observers may point out that visitor's type could be defined in one of two ways — as a function, or as an object with an apply property:
type visitorType = (this: IValue) => void;
// or
type visitorType = {
apply: (value: IValue) => void;
};
It turns out, in my case it was the latter. After adding the type declaration, I proceeded to write this incorrect code:
// This is incorrect because it doesn't pass it as an argument.
// Rather, the `this` context is set to the value.
acceptVisitors((value: IValue) => { ... });
Now, the puzzling thing is that Typescript did not show an error when I passed a function whose type was incompatible with visitorType.
Simplified example
Let's change the parameter type to a string, and walk through it.
I'm defining a type called func that is a function that requires a string argument.
type func = (param1: string) => void;
Functions by nature are callable objects that also have an apply method.
declare let f: func;
f.apply(undefined, ['str']);
// all good
Now here's the other type — an object with an apply property.
type objectWithApplyProp = {
apply: (param1: string) => void;
};
We can call the apply property, but not in the same way...
declare let o: objectWithApplyProp;
o.apply(undefined, ['str']); // Error: Expected 1 arguments, but got 2.
And objectWithApplyProp has a call signature that doesn't work with func:
o.apply('str'); // ok
f.apply('str'); // Error: The 'this' context of type 'func' is not assignable to
// method's 'this' of type '(this: string) => void'
And further tests show that f is assignable to o, but not the other way around, which makes sense... all functions are objects but not all objects are callable.
But why is f considered assignable to o? The type of objectWithApplyProp requires an apply value that matches a certain type, and func doesn't match it
A function's apply signature should be inferrable from its parameters, but typescript doesn't seem to be inferring it.
So, any feedback is welcome. Am I wrong, or is there a limitation in Typescript? Is it a known issue? Thanks

So this is a technical reason of why it's happening, and a workaround:
Typescript's built-in lib/es5.d.ts declaration file defines Function.apply with parameters of type any. Also it defines Function.prototype as any.
interface Function {
apply(this: Function, thisArg: any, argArray?: any): any;
call(this: Function, thisArg: any, ...argArray: any[]): any;
bind(this: Function, thisArg: any, ...argArray: any[]): any;
toString(): string;
prototype: any;
readonly length: number;
// Non-standard extensions
arguments: any;
caller: Function;
}
And I guess all function expressions are given the Function type by default.
So the function was allowed to be assigned to the object with the incompatible apply property because the function did not have a strongly typed apply method, based on the built-in Function types. Therefore typescript could not determine that the apply signatures were different.
Typescript 3.2 introduces CallableFunction which has generic arguments on its apply declaration. But I haven't figured out how to make it fix this problem.
A workaround is to define a stronger function type and manually assign it to the function. The workaround is a bit tedious, but it works.
interface func extends Function {
(param1: string): void;
// manually define `apply
apply<T, R> (thisArg: T, args: [string]): R;
}
interface objectWithApplyProp { // unchanged
apply: (param1: string) => void;
}
// Now we have proper errors here:
o = f;
// Type 'func' is not assignable to type 'objectWithApplyProp'.
// Types of property 'apply' are incompatible.
// Type '<T, R>(thisArg: T, args: [string]) => R' is not assignable to type '(param1: string) => void'. ts(2322)
f = o;
// Type 'objectWithApplyProp' is missing the following properties from type 'func': call, bind, prototype, length, and 2 more. ts(2740)

Related

A strange mismatch not noticed by Typescript. How is this possible?

Apparently, Typescript doesn't seem to recognize the difference between an object like {} and a generic array [] by accepting last one as input of a function that requires an object with {}'s structure.
To resume my problem, this is a simplified example to replicate it:
type test = { [key: string]: any };
let x: test = ["x", "y", "z"];
Actually, Typescript seems to accept this. How is this possible?
Note: The situation I ran into is more similar to this:
type fooType = { [key: string]: any };
const fooFunction<T extends fooType>(input: T) => // code...
fooFunction([]); // No red underline
But you can consider the first example. It's the same.
The main idea is to create a function that accepts only objects with a key (type string) and a value of any type.
Thank you in advance for the answers!
Differentiating between plain objects and other things (like arrays, or even functions) can be frustrating in JavaScript (and therefore Typescript).
Since an array is an object, you need a type that excludes arrays. For completeness, you may also want to exclude other non-plain objects, like functions, dates, regexes, etc, but I'll just focus on arrays.
Using your example, here are some approaches:
1. Exclude objects with numeric indexes
function fooFunction<T extends {
[key: string]: any,
[index: number]: never
}>(input: T) { }
fooFunction(['']); // Will have red underline!
fooFunction([]); // This will NOT have an underline!
In the above case, we're saying that T cannot have any numeric indexes. There is an edge case, though: an empty array has type never[], which also has no numeric indexes!
2. Exclude array-specific fields
Another approach is to identify some property common to arrays that won't be in any of the objects you plan to pass through your function:
function fooFunction<T extends {
map?: never,
}>(input: T) { }
fooFunction(['']); // Will have red underline!
fooFunction([]); // So will this!
3. Narrow the parameter type
The cleanest approach is to narrow your generic at the parameter to exclude arrays. The following example uses a utility type that returns never for lots of non-plain-object inputs (but not all of them):
type FancyObject = any[]|Function|Date|RegExp|Error
type PlainObject<T> = T extends FancyObject
? never
: T extends { [key: string]: any }
? T
: never;
function fooFunction<T>(input: PlainObject<T>) {}
fooFunction(['']); // Will have red underline!
fooFunction([]); // So will this!
fooFunction({ hello: 'world' }) // This is fine!

Wrap a problematic npm package, while maintaining type information

The Problem
We're authoring an npm package containing React components that will be used in various (internal) web sites. There is a problematic npm package dependency that we are forced to use in our react .tsx files, that has these problems:
It doesn't expose any useful types despite having .d.ts files in it... they're empty.
It tries to run when required or imported server-side, instead of waiting until called, so we have to avoid a top-level import and instead do if (window) { const module = require('package-name') } and then use it inside that block only.
It is a frequent source of errors so everything in that library needs to be run inside of a try ... catch block.
Well, At Least We Have Types
We have already created our own types file which addressed problem #1:
// problematic-package-types.d.ts
declare module 'problematic-package' {
function doErrorProneButNecessaryThing(
foo: Record<string, unknown>,
bar: string
): void
}
The Needed Solution
The long term solution is to fix this problematic library and we're looking into how to get that done (but it's not in our direct control).
In the short term, though, we need a solution now.
Note that we are configuring dynamic requires in our npm package bundler to import them only at use-time, not treating them like other imports/requires. As our package is consumed inside other applications, we don't have full control over how that application bundling works or when the components are required, so our components may end up being required server-side when they shouldn't, and we have to tolerate that. We're still learning about some aspects of this.
My Wild (But Failed) Stab
My goal is to do something more DRY like this, where we solve all three problems of strong typing, detecting server-side execution & doing nothing, and adding error handling:
// hoping to leverage our module declaration above without importing anything
import type * as ProblematicPackage from 'problematic-package'
import wrapProblematicRequire from '../utils/my-sanity-preserving-module'
const wrappedProblematicPackage = wrapProblematicRequire<ProblematicPackage>()
// then later...
const foo: Record<string, unknown> = { property1: 'yes', property2: false }
const bar = 'yodeling'
wrappedProblematicPackage.invokeIfWindowReady(
'doErrorProneButNecessaryThing',
foo,
bar
)
However, TypeScript doesn't like the import type which unfortunately makes sense:
Cannot use namespace 'ProblematicPackage' as a type.
The Plea
How do I get the type information we've placed into problematic-package-types.d.ts to use as desired?
Or ANYTHING else. Honestly, I'm open to whatever, no matter how crude or hacky, so long as we get some clarity and reliability at call sites, with full type information as described. Suggestions/advice?
Full Details
Here is the full implementation of the wrapProblematicRequire function. I haven't tested it. It's probably awful. I'm sure it could be far better but I don't have time to get this helper module super clean right now. (My attempt to handle function type information isn't quite right.)
type Func = (...args: any[]) => any
type FunctionNames<T, TName extends keyof T> = T[TName] extends Func ? TName : never
type FunctionNamesOf<T> = FunctionNames<T, keyof T>
const wrapProblematicRequire = <T>(packageName: string) => ({
invokeIfWindowReady<TName extends FunctionNamesOf<T>>(
name: T[TName] extends Func ? TName : never,
...args: T[TName] extends Func ? Parameters<T[TName]> : never
): T[TName] extends Func ? ReturnType<T[TName]> : never {
if (!window) {
// #ts-ignore
return undefined
}
try {
// #ts-ignore
return require(packageName)[name] as T[TName](...args)
} catch (error: unknown) {
// ToDo: Log errors
// #ts-ignore
return undefined
}
}
})
export default wrapProblematicRequire
P.S. await import('problematic-package') didn't seem to work. Yes, problems abound.
Cannot use namespace 'ProblematicPackage' as a type.
Well, you can get the typeof that namespace, which seems to be what you want.
To test this, I setup the following:
// problem.js
export function doErrorProneButNecessaryThing(n) {
return n;
}
export function doErrorProneButNecessaryThing2(s) {
return s;
}
console.log('did side effect');
// problem.d.ts
export function doErrorProneButNecessaryThing(n: number): number;
export function doErrorProneButNecessaryThing2(s: string): string;
And now you can do:
import type * as ProblemNs from './problem';
type Problem = typeof ProblemNs;
// works
type A = Problem['doErrorProneButNecessaryThing'] // type A = (n: number) => number
Then the wrapProblematicRequire function just takes the name of the function as a generic, pulls the args for it, and pulls the return type.
const wrapProblematicRequire = <TName extends FunctionNamesOf<Problem>>(
name: TName,
...args: Parameters<Problem[TName]>
): ReturnType<Problem[TName]> | undefined => {
if (!window) return;
const problem = require('./problem'); // type is any, but types are enforced above
try {
return problem[name](...args);
} catch (err) {
console.log('error!');
}
};
Here require('./problem') returns the any type, but the generics keep everything key safe as long as typeof ProblemNs can be trusted.
Now to test that:
console.log('start');
const result: number = wrapProblematicRequire(
'doErrorProneButNecessaryThing',
123
);
console.log('end');
Which logs:
start
did side effect
end
Which seems to work!
Codesandbox

TS: Cannot invoke an expression whose type lacks a call signature when defined dynamically, but it works

I'm still quite new to typescript, so please be gentle with me if I'm doing something with no sense for this technology!
The problem that I'm trying to solve is having a dynamic way to define how my application errors should be structured, but leaving to the users the faculty to enrich the messages.
So I tried to create this logic in a module that could be extended easily from the application, but I'm currently facing the problem:
Error:(35, 18) TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'ErrorMessage' has no compatible call signatures.
What I thought it was a good idea (but please tell me if I'm wrong), was to use a register and a map to have the possibility to extend this mapping every time I want. So I created my ErrorMessage interface to be like the following:
export interface ErrorMessage {
actionMessage: string;
actionSubject: string;
originalErrorMessage?: string;
toString: () => string;
}
and a register for these, called ErrorResponseRegister, as it follows:
export enum defaultErrors {
ExceptionA = 'ExceptionA',
ExceptionB = 'ExceptionB',
}
export class ErrorResponseRegister {
private mapping: Map<string, ErrorMessage>;
constructor() {
this.mapping = new Map()
.set(defaultErrors.ExceptionA, exceptionAErrorMessage)
.set(defaultErrors.ExceptionB, exceptionBErrorMessage);
}
}
So at the end, every ErrorMessage function should look like:
export function exceptionAErrorMessage(originalErrorMessage?: string): ErrorMessage {
return {
enrichment1: "Something happened",
enrichment2: "in the application core",
originalErrorMessage: originalErrorMessage,
toString(): string {
return `${this.enrichment1} ${this.enrichment2}. Original error message: ${originalErrorMessage}`;
},
};
}
Please note I haven't used classes for this ones, as it doesn't really need to be instantiated
and I can have a bunch of them where the toString() method can vary. I just want to enforce the errors should have an enrichment1 and enrichment2 that highlight the problem in a better way for not-technical people.
So, now, back to code. When I'm trying to use the exceptionAErrorMessage statically, I can't see any problem:
console.log(exceptionAErrorMessage(originalErrorMessage).toString())
But when I try dynamically, using the map defined in the ErrorResponseRegister, something weird happens:
// In ErrorResponseRegister
public buildFor(errorType: string, originalErrorMessage?: string): Error {
const errorMessageBuilder = this.mapping.get(errorType);
if (errorMessageBuilder) {
return errorMessageBuilder(originalErrorMessage).toString();
}
return "undefined - do something else";
}
The code works as expected, the error returned is in the right format, so the toString function is executed correctly.
BUT, the following error appears in the IDE:
Error:(32, 18) TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'ErrorMessage' has no compatible call signatures.
The line that causes the problem is
errorMessageBuilder(originalPosErrorMessage).toString()
Can someone help me to understand what I'm doing wrong?
It looks like your problem is you've mistyped mapping... it doesn't hold ErrorMessage values; it holds (x?: string)=>ErrorMessage values:
private mapping: Map<string, (x?: string) => ErrorMessage>;
What's unfortunate is that you initialize this variable via new Map().set(...) instead of the using an iterable constructor argument.
The former returns a Map<any, any> which is trivially assignable to mapping despite the mistyping. That is, you ran smack into this known issue where the standard library's typings for the no-argument Map constructor signature produces Map<any, any> which suppresses all kinds of otherwise useful error messages. Perhaps that will be fixed one day, but for now I'd suggest instead that you use the iterable constructor argument, whose type signature declaration will infer reasonable types for the keys/values:
constructor() {
this.mapping = new Map([
[defaultErrors.ExceptionA, exceptionAErrorMessage],
[defaultErrors.ExceptionB, exceptionBErrorMessage]
]); // inferred as Map<defaultErrors, (orig?: string)=>ErrorMessage>
}
If you had done so, it would have flagged the assignment as an error with your original typing for mapping (e.g., Type 'Map<defaultErrors, (originalErrorMessage?: string | undefined) => ErrorMessage>' is not assignable to type 'Map<string, ErrorMessage>'.) Oh well!
Once you make those changes, things should behave more reasonably for you. Hope that helps; good luck!
Link to code

Property does not exist on a function's return value of multiple types

I'm using typescript to write NodeJS program.
In this program, I import a node module called ts-md5, in which there is a function hashStr(), it could return a value of string or Int32Array.
I need to do things like this in my program:
Md5.hashStr(str).toUpperCase();
However, the compiler complains error:
error TS2339: Property 'toUpperCase' does not exist on type 'string | Int32Array'.
The program runs successfully. Because it always returns string during runtime. But I want to know if there is a way to get rid of this annoying error?
You can use a type guard, or a type assertion.
type guard
let hash = Md5.hashStr(str);
if (typeof hash === 'string') {
hash = hash.toUpperCase();
}
type assertion
let hash = (<string>Md5.hashStr(str)).toUpperCase();
The benefit of the type guard is that it technically safer - because if you ever did get something that wasn't a string at runtime, it would still work. The type assertion is simply you overriding the compiler, so it isn't technically as safe, but it is entirely erased and therefore results in the same runtime code you have at the point you have the error.
hashStr is declared in ts-md5 typings as
static hashStr(str: string, raw?: boolean): string | Int32Array;
Looking at the implementation, is seems that it returns Int32Array when raw is true, and returns string otherwise.
Given that declaration, you can't do much better than use type assertion:
let hash = (Md5.hashStr(str) as string).toUpperCase()
The proper way to express that return type is dependent on the parameter in TypeScript is via overload declarations. Something like this should work:
static hashStr(str: string): string;
static hashStr(str: string, raw: false): string;
static hashStr(str: string, raw: true): Int32Array;
static hashStr(str: string, raw: boolean): Int32Array | string;
static hashStr(str: string, raw?: boolean): string | Int32Array {
// implementation goes here...
}
I'd suggest posting an issue with ts-md5 about this.

How to work around "type interface has no field or method" error?

I want to write an abstraction to the mgo API:
package manager
import "labix.org/v2/mgo"
type Manager struct {
collection *mgo.Collection
}
func (m *Manager) Update(model interface{}) error {
return m.collection.UpdateId(model.Id, model)
}
When compiling I get "model.Id undefined (interface{} has no field or method Id)" which itself is obvious.
Is this a totally wrong approach from my side or is there an easy workaround how to let the compiler "trust" that there will be an Id property on runtime on passed structs.
You could defined an interface which declares an Id function
type Ider interface {
Id() interface{}
}
If your model is an Ider, then your function will work.
func (m *Manager) Update(model Ider) error {
Considering the mgo#Collection.UpdateId() function takes interface{}, it will accept an Ider.

Resources