Devin
Devin

Reputation: 21

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

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.

Upvotes: 1

Views: 567

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187134

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.

Upvotes: 2

Related Questions