Why does typescript complain when I import a component, but not when the component is defined in the same file? - reactjs

I have an issue where I get two different results from Typescript's type check when I import a component from another file vs define the component in the same file where it's used.
I made a sandbox to describe question in more detail: https://codesandbox.io/s/typescript-error-1l44t?file=/src/App.tsx
If you look at Example function, I'm passing in an additional parameter z, which shouldn't be valid, therefore I'm getting an error (as expected).
However if you enable L15-L22 where ExampleComponent is defined in the same file, then disable or remove the ExampleComponent import from './component' on L2, suddenly Typescript stops complaining.
Any help would be appreciated. Thank you!
If there's any extra information I can give, please let me know.

This is because your are refining a type in one scope, but if you export that value you don't also get those refinements.
In other words, when you create the value in the same file, Typescript can infer a more specific subtype. But when it's in another file it just imports whatever the most broad type that it could possibly be.
Here's a simpler example:
// test.ts
export const test: string | number = 'a string';
test.toUpperCase(); // works
This works because typescript observed that test is a string because of the assignment of a string literal. There is no way that, after executing the code of that file, test could be a number.
However, test is still typed as string | number. It's just that in this scope typescript can apply a refinement to a more narrow type.
Now let's import test into another file:
// other.ts
import { test } from './test'
test.toUpperCase() // Property 'toFixed' does not exist on type 'string | number'.
Refinements only get applied in the scope where they were refined. That means that you get the more broad type when you export that value.
Another example:
// test.ts
export const test = Math.random() > 0.5 ? 'abc' : 123 // string | number
if (typeof test === 'string') throw Error('string not allowed!')
const addition = test + 10 // works fine
// other.ts
import { test } from './test'
const addition = test + 10 // Operator '+' cannot be applied to types 'string | number' and 'number'.(2365)
In this case the program should throw an exception if a string is assigned. In the test.ts file, typescript knows that and therefore knows that test must be a number if that third line is executing.
However, the exported type is still string | number because that's what you said it was.
In your code, React.ComponentType<P> is actually an alias for:
React.ComponentClass<P, any> | React.FunctionComponent<P>
Typescript notices that you are assigning a function, and not a class, and refines that type to React.FunctionComponent<P>. But when you import from another file it could be either, so typescript is more paranoid, and you get the type error.
And, lastly, for a reason I haven't yet figured out, your code works with a function component, but not with a class component. But this should at least make it clear why there's a difference at all.

Related

property map does not exist on type string | Array<string>

This is how I have defined defined default value in interface , because sometimes it is string and othertimes it is array, I am doing map, when I am sure it will be array. however I get the ts error 2339.
interface ABC {
defaultValue?: string | ILabelValue[];
}
// below is my code
x.defaultValue.map(e=>e.value) // I am getting error map does not exist on type string | ILabelValue[]
Seems like if we do a type check in the code, then typescript would stop complaining, so I got rid of the error like this.
if(x.defaultValue instanceof Array){
x.defaultValue.map(e=>e.value)
}
Now there are no typescript compilation errors for the above code.
so we have to do one more check, however I would also like if there is a way to tell typescript that I know what I am doing and I am sure that over here, defaultValue will always be array.

TypeScript Discriminated Union with Optional Discriminant

I've created a discriminated union that's later used to type props coming into a React component. A pared down sample case of what I've created looks like this:
type Client = {
kind?: 'client',
fn: (updatedIds: string[]) => void
};
type Server = {
kind: 'server',
fn: (selectionListId: string) => void
};
type Thing = Client | Server;
Note that the discriminant, kind, is optional in one code path, but is defaulted when it is destructured in the component definition:
function MyComponent(props: Thing) {
const {
kind = 'client',
fn
} = props;
if (kind === 'client') {
props.fn(['hey']);
// also an error:
// fn(['hey'])
} else {
props.fn('hi')
// also an error:
// fn('hey')
}
}
What I'm trying to understand is what's going on with this conditional. I understand that the type checker is having trouble properly narrowing the type of Thing, since the default value is separate from the type definition. The oddest part is that in both branches of the conditional it insists that fn is of type (arg0: string[] & string) => void and I don't understand why the type checker is trying to intersect the parameters here.
I would have expected it to be unhappy about non-exhaustiveness of the branches (i.e. not checking the undefined branch) or just an error on the else branch where the 'server' and undefined branches don't line up. But even trying to rearrange the code to be more explicit about each branch doesn't seem to make any impact.
Perhaps because the compiler simply can't narrow the types so tries an intersection so it doesn't matter which path--if the signatures are compatible then it's fine to call the function, otherwise you basically end up with a never (since string[] & string is an impossible type)?
I understand that there are a variety of ways I can resolve this via user-defined type predicates or type assertions, but I'm trying to get a better grasp on what's going on here internally and to find something a bit more elegant.
TS Playground link
It's an implementation detail in TS. Types are not narrowed when you are storing the value in a different variable. The same issue exists for the square bracket notation. You can refer to this question, it deals with a similar issue. Apparently, this is done for compiler performance.
You can fix your issue by using both props.fn and props.kind.
playground
Or write a type guard function.

Why does passing the plot type ('scatter', 'bar', etc.) as string variable does not work? (using <Plot ... /> from react-plotly.js)

I am using TypeScript, React and Plotly in my project and would like to know if it is possible to pass the plot type specification using a variable. Something like this (which is not a working code example and only used to indicate what I mean)
import Plot from 'react-plotly.js';
var plotType: string = 'bar';
return (
<Plot
data={[{
x: [1,2,3,4],
y: [1,2,3,4],
type: 'bar' // this works
type: plotType // this doesn't
}]}
/>
);
It's not really an issue since I go about the whole 'data' thing using a state property, but I still don't get why it works with the literal string but not with a variable.
The error is something like 'string' cannot be assigned since '"bar"|"scatter"|"line" ...' is expected.
For a working example I can only refer to the react-plotly github repos, where one can use the given quickstart example and try to substitute the string in type: 'scatter' with a variable.
PS.: I am quiet new to TS or JS in general so I might be using wrong/misleading terms unknowningly.
It isnt entirely evident in their docs but, this is actually a TS feature that the plotly devs are using.
I can say
type MyType = string;
then if I say
const myVar = '123';
TS will be perfectly happy. On the other hand, I could restrict other devs on what actual values they assign to MyType like
type MyType = 'bar' | 'pie' | 'scatter';
which is exactly what plotly has done. and then if a dev says
<Plot type={'123'} />
TS will throw an error because '123' isnt one of the options.
Now, you say, but my plotType variable IS one of those options. While thats true, you typed it as :string. So, when TS type checks is compares the PlotType type inside Plotly which is a limited set of strings to the string type. Thus, you have a mismatched type. In your case you could have said
var plotType: Plotly.PlotType = 'bar';
since the types now match, this would be acceptable. In fact, in this case you wouldnt even be able to accidently change the value of plotType to something not a part of Plotly.PlotType because TS would complain now.

Error exporting type in Flow

I am trying to export a type per the guidelines in the Flow docs. In global.js I have
export type alertConfig = {
type: string,
message: string,
exists: boolean,
};
In another file I import and attempt to use this type:
import type alertConfig from "./global.js"
type State = {
alertConf: alertConfig,
buttonLoading: boolean,
};
which gives me the following flow error: Cannot use object literal as a type because object literal is a value. To get the type of a value use typeof.
This is strange because when I write typeof(alertConfig), I get the error Cannot reference type alertConfig [1] from a value position. So the imported object alertConfig is being recognized as a type, but for some reason the original code doesn't work.
I can't tell you exactly why because I'm new to flow as well, but you need to import types with squiggle brackets around them. So your code should be:
import type {alertConfig} from "./global.js"

Typescript Class.prototype.MyFunction makes errors but working

I can't explain why I get an error but code works. Is that compiler bug? (I use Visual Studio Code with Angular 2)
class A
{
fun(a: number)
{
return a+2;
}
}
A.prototype.F = function() { return "F here!"+this.fun(1); } // This makes error: The property 'F' does not exist on value of type 'A'
var a: A = new A();
console.log(a.F());
And bonus: This is not working at all! (no access to this.fun())
A.prototype.F2 = () => { return "F2 here!"+this.fun(1); } // ()=>{} is not working! cause _this is not defined!
...
console.log(a.F2());
Edit #1
As #seangwright said I need to use Module Augmentation but...
As far as it's working with simple example with my A class I can't make it work with Angular's ComponentFixture. This should solve my problem if I do this like in Typescript example:
declare module '#angular/core/testing' // I was trying without this line and with 'global' instead of '#angular/core/testing' but nothing helps
{
interface ComponentFixture<T>
{
TestOf(cssSelector: string): string;
}
}
But I still get an error:
'ComponentFixture' only refers to a type, but is being used as a value
here.'
at this point:
ComponentFixture.prototype.TextOf = function(cssSelector: string): string
{
...
}
There is even more errors, for example when I try to use it:
let fixture: ComponentFixture<EditableValueComponent>;
fixture = TestBed.createComponent(EditableValueComponent);
I got:
'ComponentFixture' is not assignable to type
'ComponentFixture'. Two different types with
this name exist, but they are unrelated. Property 'TestOf' is
missing in type 'ComponentFixture'
So again: Code works but has many compilation errors. Or maybe I'm missing something obvious?
I get the feeling you are a C# developer based on how you format your code.
Part 1
In Typescript once you declare your class, the type system expects it to have the properties (shape) you define and that's it.
The more of the type system you want to use, the less dynamic your objects will/can be.
That said, the reason your code runs (transpiles) correctly is because this is an error in the context of Typescript's structural type system, not Javascript's dynamic type system. So Typescript will tell you A doesn't have a property F at compile time, but Javascript doesn't care that it's added at runtime.
One solution is to merge the class with an interface
class A {
fun(a: number) {
return a + 2;
}
}
interface A {
F(): string;
}
A.prototype.F = function () { return "F here!" + this.fun(1); }
var a: A = new A();
console.log(a.F());
Another would be to temporarily abandon the type system
class A {
fun(a: number) {
return a + 2;
}
}
(A.prototype as any).F = function () { return "F here!" + this.fun(1); }
var a: A = new A();
console.log((a as any).F());
But that becomes verbose and prone to errors and loses the benefits that a type system brings.
You mention you are using Typescript with Angular 2. You could write in ES2015 if you wanted a more dynamic syntax. But then you will lose some of the benefits that Angular 2 gets from using Typescript (better tooling, smaller deployments).
Part 2
The reason your second example doesn't work at all has nothing to do with Typescript and everything to do with Scope (or execution context) in Javascript, specifically ES2015 arrow functions.
An arrow function does not create its own this context, so this has its original meaning from the enclosing context.
Unlike in your first example you are not using the traditional function declaration syntax and instead are using the () => {} arrow function syntax. With your first example
A.prototype.F = function() { return "F here!"+this.fun(1); }
this refers to whatever context F() is going to be executing in. Since you define it on the prototype of A it is going to be executing in the context of A. A has a .fun() method so this.fun() is going to be the same one defined in your class above.
With your second example, F2 is not going to be executing in the context of A despite being defined as a method of its prototype. The arrow function syntax is instead going to allow F2 to run in the context of the enclosing context which is the global window object unless you are running in strict mode in which case
in browsers it's no longer possible to reference the window object through this inside a strict mode function.
So this will be undefined and calling fun() on undefined is going to throw an error.
Try adding a console.log(this) to your F2 function.
A.prototype.F2 = () => { console.log(this); return "F2 here!"+this.fun(1); }
When you run the transpiled Javascript you will probably see Window logged out to the console, and then probably an error like Uncaught TypeError: _this.fun is not a function
Use the Typescript Playground to write some Typescript, see what the tooling tells you, what transpiled Javascript is created and then run it to see if your Javascript is correct.

Resources