Comments (6)
A somewhat smaller repro would be this:
type IsNumber<T> = [T] extends [number] ? true : false
type Shortcut<T> = false[] extends IsNumber<T>[] ? 0 : 1;
// ^? type Shortcut<T> = 1
TS tries to check if the check type is assignable to a permissive instantiation of the extends type. That permissive instantiation is true[]
here so it ends up checking false[] extends true[] ? 0 : 1
and it determined that this can never be true and picked up the false type as the result.
Some extra mechanisms that avoid over-eager simplifications were introduced in #56004 but that was only when computing constraints of conditional types and in this situation forConstraint
(the variable introduced there) is false
.
This is the most relevant piece of the implementation: link. Perhaps the biggest surprise (for me) is that this extends type (IsNumber<T>[]
) is not deferred (it depends on a type variable after all).
The definition of isDeferredType
is pretty simple:
function isDeferredType(type: Type, checkTuples: boolean) {
return isGenericType(type) || checkTuples && isTupleType(type) && some(getElementTypes(type), isGenericType);
}
I have never formed a good mental model for what a generic type is internally (again, this one depends on a type variable but it's "just" a type reference to Array
and this is not called a generic type internally, even if it has generic type arguments).
from typescript.
That's weird that the so-called "permissive instantiation" of IsNumber would be true[]
A permissive instantiation is an instantiation that maps type parameters to a wildcard type (an internal type of any
). But even with a regular any
we get this:
type IsNumber<T> = [T] extends [number] ? true : false
type Result = IsNumber<any>
// ^? type Result = true
Especially weird to me that this works again if I define IsFalse with tuple types instead of array types.
To quote the source code:
// When the check and extends types are simple tuple types of the same arity, we defer resolution of the
// conditional type when any tuple elements are generic. This is such that non-distributable conditional
// types can be written `[X] extends [Y] ? ...` and be deferred similarly to `X extends Y ? ...`.
const checkTuples = isSimpleTupleType(checkTypeNode) && isSimpleTupleType(extendsTypeNode) &&
length((checkTypeNode as TupleTypeNode).elements) === length((extendsTypeNode as TupleTypeNode).elements);
I don't know why tuples are special this way. I understand that wrapping types in tuples might be a common way to make your conditional types non-distributive but I don't think I've ever seen this to be documented as the only way to do that. My personally mental model was always that you can make conditional types non-distributive by simply making check type "non-naked" and T[]
is something that I'd call non-naked.
But as I alluded to in my previous comment - the problem is not exclusive to arrays, likely just any other type reference (other than a tuple) with generic type parameters exhibits the same problem:
type IsNumber<T> = [T] extends [number] ? true : false;
type Shortcut<T> = Record<string, false> extends Record<string, IsNumber<T>> ? 0 : 1;
// ^? type Shortcut<T> = 1
from typescript.
I feel like there was a recent issue about exactly this...
from typescript.
That's weird that the so-called "permissive instantiation" of IsNumber<T>
would be true[]
; I'd expect it to be boolean[]
(or maybe never[]
, i.e. intersection of true and false types)1 since true
and false
are disjoint and there's seemingly no reason to prefer the true branch of the conditional over the false branch. Of course we'd have the same problem in the end, since the eager evaluation itself is the problem here, but c'est la vie.
Footnotes
-
I'm interpreting "permissive" here to mean "compatible with whatever ultimately comes out of the conditional type", which of course depends on things like variance, how the true and false types are related, etc. ↩
from typescript.
Especially weird to me that this works again if I define IsFalse
with tuple types instead of array types.
type IsFalse2<T extends boolean> = [false] extends [T] ? true : false
type Shortcut2<T> = IsFalse2<IsNumber<T>> extends true ? 0 : 1;
type Indirect2 = Shortcut2<string>;
// ^? type Indirect2 = 0
from typescript.
@Andarist I believe it’s been stated by a maintainer (@ahejlsberg, perhaps?) that, while the documented behavior is “naked type parameters are distributive”, there’s special logic to handle the pattern [T] extends [Foo] ? …
, for perf reasons I believe, because that’s the most common way to write non-distributive conditionals.
from typescript.
Related Issues (20)
- Error: Debug Failure. False expression: Expected the specifier to be a default export HOT 1
- type assertions affect the type narrowing of the subsequent code HOT 6
- Missing interface definition for Highlight API HOT 1
- Improve new issue template selection - reorder and handle "this is crash" check better HOT 3
- Union type breaks type safety of dynamic object keys HOT 2
- Asserting changes type of original variable inline (the change reverts in subsequent lines) HOT 10
- "declare class" in a global is not visible in external files HOT 7
- Mapped type reported as incompatible when passed through another mapped type (5.4 regression) HOT 1
- Shorten error spans for missing key errors reported on object literals HOT 3
- Array prototype extensions fail circularly with `...args: T[]`, but not `args: T[]` HOT 6
- Inconsistent behaviour of es6 and esnext property initializers when using experimental property decorators HOT 3
- Spread operator doesn't remove readonly from object properties HOT 3
- Add option to throw error when `!` (NonNull expression) is used on a non-nullish expression HOT 2
- Inconsistent Type Narrowing with `never` Return Type Between Arrow and Traditional Functions HOT 2
- Class infers wrong generic parameter since e1874f3 HOT 2
- Promise returned from sync `dispose` method should not be awaited when disposing an asyc-disposable HOT 1
- Import ellision emit bug: usage of imports only in keys of interfaces does not result in those imports being elided as type-only
- `noUncheckedIndexedAccess` does not narrow properly when iterating with `for...in` HOT 4
- Omit private symbols from type HOT 6
- TS5055: Typescript includes files in "dist" folder for compilation when `rootDir` is not `./` but `./src` even after explicitly adding "dist" to `exclude` HOT 7
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from typescript.