-
Notifications
You must be signed in to change notification settings - Fork 13k
Description
Search Terms
- referential transparency
- substitution
- looked at Common Feature Requests on the FAQ
Suggestion
Depending on the context, TypeScript will infer some types in slightly different ways:
"foo"
can be inferred as either the string literal type"foo"
orstring
.[1, 2]
can be inferred as either the tuple type[number, number]
or asnumber[]
.
One consequence of this is that referential transparency is sometimes broken. See examples below. (Disclaimer: I'm not entirely sure whether I'm using this term correctly!)
I believe this can be a real source of confusion for JavaScript developers coming to TypeScript, since it flags correct code as incorrect and generates errors that require understanding of some advanced features of TypeScript (e.g. string literal types). The solution often involves digging around through type declaration files with no guarantee of success (see also below).
One idea for fixing this would be to base type inference on how a value is used later on. But I assume that has many downsides. Mainly I wanted to raise this issue with the TypeScript team and hear if you consider this a problem or WAI or just impossible to get right.
Examples
Simple example
In plain-old JavaScript, extracting a constant is always OK:
doSomething(value);
// -->
const v = value;
doSomething(v);
This is sometimes OK in TypeScript, sometimes not. Here's an example where it's not:
function panTo(latLng: [number, number]) { /* ... */ }
panTo([10, 20]); // ok
const x = [10, 20];
panTo(x); // error:
// Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
// Property '0' is missing in type 'number[]'.
Real world example
Real world examples of this often come up with third-party type declarations. For example, in react-mapbox-gl you specify the initial center, zoom, pitch and bearing of the map as tuples:
<Map
center={[-74, 40.7]}
zoom={[14.5]}
pitch={[45]}
bearing={[-17.6]}
}>
{ /* ... */ }
</Map>
If you try to factor this out into a constant to avoid magic numbers you get an error:
const INITIAL_VIEW = {
center: [-74, 40.7],
zoom: [14.5],
pitch: [45],
bearing: [-17.6],
};
return (
<Map {...INITIAL_VIEW} }>
{ /* ... */ }
</Map>
); // error!
The error is a bit of a mouthful:
Type '{ children: Element[]; onClick: () => void; center: number[]; zoom: number[]; pitch: number[]; bearing: number[]; style: string; containerStyle: { height: string; width: string; }; }' is not assignable to type 'Partial<Pick<Readonly<{ children?: ReactNode; }> & Readonly<Props & Events>, "zoom" | "center" | "bearing" | "pitch" | "movingMethod" | "onStyleLoad">>'.
Types of property 'zoom' are incompatible.
Type 'number[]' is not assignable to type '[number]'.
Property '0' is missing in type 'number[]'.
The only solution I'm aware of is to dig through the internals of the type declarations until you find the correct type:
import {Props as MapProps} from 'react-mapbox-gl/lib/map';
const INITIAL_VIEW: Partial<MapProps> = {
center: [-74, 40.7],
zoom: [14.5],
pitch: [45],
bearing: [-17.6],
};
return (
<Map {...INITIAL_VIEW} }>
{ /* ... */ }
</Map>
); // ok
And this assumes that the relevant type is exported. If not you may have to just give up and throw in an as any
.
Both of these examples involved tuples being inferred as arrays, but the same thing can happen with string literals being inferred as string
.
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript / JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. new expression-level syntax)