Skip to content

Commit e6fe96c

Browse files
authored
Add NoInfer<T> intrinsic represented as special substitution type (#56794)
1 parent fea4a45 commit e6fe96c

File tree

14 files changed

+1185
-11
lines changed

14 files changed

+1185
-11
lines changed

src/compiler/checker.ts

+45-9
Original file line numberDiff line numberDiff line change
@@ -1366,13 +1366,15 @@ const enum IntrinsicTypeKind {
13661366
Lowercase,
13671367
Capitalize,
13681368
Uncapitalize,
1369+
NoInfer,
13691370
}
13701371

13711372
const intrinsicTypeKinds: ReadonlyMap<string, IntrinsicTypeKind> = new Map(Object.entries({
13721373
Uppercase: IntrinsicTypeKind.Uppercase,
13731374
Lowercase: IntrinsicTypeKind.Lowercase,
13741375
Capitalize: IntrinsicTypeKind.Capitalize,
13751376
Uncapitalize: IntrinsicTypeKind.Uncapitalize,
1377+
NoInfer: IntrinsicTypeKind.NoInfer,
13761378
}));
13771379

13781380
const SymbolLinks = class implements SymbolLinks {
@@ -6749,7 +6751,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
67496751
return visitAndTransformType(type, type => conditionalTypeToTypeNode(type as ConditionalType));
67506752
}
67516753
if (type.flags & TypeFlags.Substitution) {
6752-
return typeToTypeNodeHelper((type as SubstitutionType).baseType, context);
6754+
const typeNode = typeToTypeNodeHelper((type as SubstitutionType).baseType, context);
6755+
const noInferSymbol = isNoInferType(type) && getGlobalTypeSymbol("NoInfer" as __String, /*reportErrors*/ false);
6756+
return noInferSymbol ? symbolToTypeNode(noInferSymbol, context, SymbolFlags.Type, [typeNode]) : typeNode;
67536757
}
67546758

67556759
return Debug.fail("Should be unreachable.");
@@ -15975,8 +15979,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1597515979

1597615980
function getTypeAliasInstantiation(symbol: Symbol, typeArguments: readonly Type[] | undefined, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
1597715981
const type = getDeclaredTypeOfSymbol(symbol);
15978-
if (type === intrinsicMarkerType && intrinsicTypeKinds.has(symbol.escapedName as string) && typeArguments && typeArguments.length === 1) {
15979-
return getStringMappingType(symbol, typeArguments[0]);
15982+
if (type === intrinsicMarkerType) {
15983+
const typeKind = intrinsicTypeKinds.get(symbol.escapedName as string);
15984+
if (typeKind !== undefined && typeArguments && typeArguments.length === 1) {
15985+
return typeKind === IntrinsicTypeKind.NoInfer ? getNoInferType(typeArguments[0]) : getStringMappingType(symbol, typeArguments[0]);
15986+
}
1598015987
}
1598115988
const links = getSymbolLinks(symbol);
1598215989
const typeParameters = links.typeParameters!;
@@ -16158,10 +16165,32 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1615816165
return links.resolvedJSDocType;
1615916166
}
1616016167

16168+
function getNoInferType(type: Type) {
16169+
return isNoInferTargetType(type) ? getOrCreateSubstitutionType(type, unknownType) : type;
16170+
}
16171+
16172+
function isNoInferTargetType(type: Type): boolean {
16173+
// This is effectively a more conservative and predictable form of couldContainTypeVariables. We want to
16174+
// preserve NoInfer<T> only for types that could contain type variables, but we don't want to exhaustively
16175+
// examine all object type members.
16176+
return !!(type.flags & TypeFlags.UnionOrIntersection && some((type as UnionOrIntersectionType).types, isNoInferTargetType) ||
16177+
type.flags & TypeFlags.Substitution && !isNoInferType(type) && isNoInferTargetType((type as SubstitutionType).baseType) ||
16178+
type.flags & TypeFlags.Object && !isEmptyAnonymousObjectType(type) ||
16179+
type.flags & (TypeFlags.Instantiable & ~TypeFlags.Substitution) && !isPatternLiteralType(type));
16180+
}
16181+
16182+
function isNoInferType(type: Type) {
16183+
// A NoInfer<T> type is represented as a substitution type with a TypeFlags.Unknown constraint.
16184+
return !!(type.flags & TypeFlags.Substitution && (type as SubstitutionType).constraint.flags & TypeFlags.Unknown);
16185+
}
16186+
1616116187
function getSubstitutionType(baseType: Type, constraint: Type) {
16162-
if (constraint.flags & TypeFlags.AnyOrUnknown || constraint === baseType || baseType.flags & TypeFlags.Any) {
16163-
return baseType;
16164-
}
16188+
return constraint.flags & TypeFlags.AnyOrUnknown || constraint === baseType || baseType.flags & TypeFlags.Any ?
16189+
baseType :
16190+
getOrCreateSubstitutionType(baseType, constraint);
16191+
}
16192+
16193+
function getOrCreateSubstitutionType(baseType: Type, constraint: Type) {
1616516194
const id = `${getTypeId(baseType)}>${getTypeId(constraint)}`;
1616616195
const cached = substitutionTypes.get(id);
1616716196
if (cached) {
@@ -16175,7 +16204,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1617516204
}
1617616205

1617716206
function getSubstitutionIntersection(substitutionType: SubstitutionType) {
16178-
return getIntersectionType([substitutionType.constraint, substitutionType.baseType]);
16207+
return isNoInferType(substitutionType) ? substitutionType.baseType : getIntersectionType([substitutionType.constraint, substitutionType.baseType]);
1617916208
}
1618016209

1618116210
function isUnaryTupleTypeNode(node: TypeNode) {
@@ -17853,7 +17882,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1785317882

1785417883
function getIndexType(type: Type, indexFlags = defaultIndexFlags): Type {
1785517884
type = getReducedType(type);
17856-
return shouldDeferIndexType(type, indexFlags) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, indexFlags) :
17885+
return isNoInferType(type) ? getNoInferType(getIndexType((type as SubstitutionType).baseType, indexFlags)) :
17886+
shouldDeferIndexType(type, indexFlags) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, indexFlags) :
1785717887
type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, indexFlags))) :
1785817888
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, indexFlags))) :
1785917889
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, indexFlags) :
@@ -19941,6 +19971,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1994119971
}
1994219972
if (flags & TypeFlags.Substitution) {
1994319973
const newBaseType = instantiateType((type as SubstitutionType).baseType, mapper);
19974+
if (isNoInferType(type)) {
19975+
return getNoInferType(newBaseType);
19976+
}
1994419977
const newConstraint = instantiateType((type as SubstitutionType).constraint, mapper);
1994519978
// A substitution type originates in the true branch of a conditional type and can be resolved
1994619979
// to just the base type in the same cases as the conditional type resolves to its true branch
@@ -25459,7 +25492,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2545925492
inferFromTypes(originalSource, originalTarget);
2546025493

2546125494
function inferFromTypes(source: Type, target: Type): void {
25462-
if (!couldContainTypeVariables(target)) {
25495+
if (!couldContainTypeVariables(target) || isNoInferType(target)) {
2546325496
return;
2546425497
}
2546525498
if (source === wildcardType || source === blockedStringType) {
@@ -25532,6 +25565,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2553225565
}
2553325566
}
2553425567
if (target.flags & (TypeFlags.IndexedAccess | TypeFlags.Substitution)) {
25568+
if (isNoInferType(target)) {
25569+
return;
25570+
}
2553525571
target = getActualTypeVariable(target);
2553625572
}
2553725573
if (target.flags & TypeFlags.TypeVariable) {

src/compiler/types.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6707,8 +6707,9 @@ export interface StringMappingType extends InstantiableType {
67076707
// Substitution types are created for type parameters or indexed access types that occur in the
67086708
// true branch of a conditional type. For example, in 'T extends string ? Foo<T> : Bar<T>', the
67096709
// reference to T in Foo<T> is resolved as a substitution type that substitutes 'string & T' for T.
6710-
// Thus, if Foo has a 'string' constraint on its type parameter, T will satisfy it. Substitution
6711-
// types disappear upon instantiation (just like type parameters).
6710+
// Thus, if Foo has a 'string' constraint on its type parameter, T will satisfy it.
6711+
// Substitution type are also created for NoInfer<T> types. Those are represented as substitution
6712+
// types where the constraint is type 'unknown' (which is never generated for the case above).
67126713
export interface SubstitutionType extends InstantiableType {
67136714
objectFlags: ObjectFlags;
67146715
baseType: Type; // Target type

src/harness/fourslashInterfaceImpl.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,7 @@ export namespace Completion {
11951195
typeEntry("Lowercase"),
11961196
typeEntry("Capitalize"),
11971197
typeEntry("Uncapitalize"),
1198+
typeEntry("NoInfer"),
11981199
interfaceEntry("ThisType"),
11991200
varEntry("ArrayBuffer"),
12001201
interfaceEntry("ArrayBufferTypes"),

src/lib/es5.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,11 @@ type Capitalize<S extends string> = intrinsic;
16481648
*/
16491649
type Uncapitalize<S extends string> = intrinsic;
16501650

1651+
/**
1652+
* Marker for non-inference type position
1653+
*/
1654+
type NoInfer<T> = intrinsic;
1655+
16511656
/**
16521657
* Marker for contextual 'this' type
16531658
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
noInfer.ts(35,13): error TS2345: Argument of type '"bar"' is not assignable to parameter of type '"foo"'.
2+
noInfer.ts(36,14): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
3+
noInfer.ts(37,14): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
4+
noInfer.ts(38,15): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
5+
noInfer.ts(39,15): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
6+
noInfer.ts(47,30): error TS2741: Property 'woof' is missing in type 'Animal' but required in type 'Dog'.
7+
noInfer.ts(53,16): error TS2345: Argument of type '{ x: number; }' is not assignable to parameter of type '{ x: number; y: number; }'.
8+
Property 'y' is missing in type '{ x: number; }' but required in type '{ x: number; y: number; }'.
9+
noInfer.ts(58,22): error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
10+
noInfer.ts(59,14): error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
11+
noInfer.ts(66,14): error TS2345: Argument of type '{}' is not assignable to parameter of type '{ foo: number; }'.
12+
Property 'foo' is missing in type '{}' but required in type '{ foo: number; }'.
13+
14+
15+
==== noInfer.ts (10 errors) ====
16+
// NoInfer<T> is erased for primitives
17+
18+
type T00 = NoInfer<string>;
19+
type T01 = NoInfer<string | number | boolean>;
20+
type T02 = NoInfer<undefined>;
21+
type T03 = NoInfer<"foo">;
22+
type T04 = NoInfer<`foo${string}`>;
23+
type T05 = NoInfer<`foo${string}` & `${string}bar`>;
24+
type T06 = NoInfer<{}>;
25+
26+
// NoInfer<T> is preserved for object types
27+
28+
type T10 = NoInfer<string[]>;
29+
type T11 = NoInfer<{ x: string }>;
30+
31+
// NoInfer<T> is erased if it has no effect
32+
33+
type T20<T> = NoInfer<NoInfer<T>>;
34+
type T21<T> = NoInfer<NoInfer<T> & string>;
35+
type T22<T> = NoInfer<NoInfer<T> & string[]>;
36+
37+
// keyof NoInfer<T> is transformed into NoInfer<keyof T>
38+
39+
type T30 = keyof NoInfer<{ a: string, b: string }>;
40+
type T31<T> = keyof NoInfer<T>;
41+
type T32 = { [K in keyof NoInfer<{ a: string, b: string }>]: K };
42+
43+
declare function foo1<T extends string>(a: T, b: NoInfer<T>): void
44+
declare function foo2<T extends string>(a: T, b: NoInfer<T>[]): void
45+
declare function foo3<T extends string>(a: T, b: NoInfer<T[]>): void
46+
declare function foo4<T extends string>(a: T, b: { x: NoInfer<T> }): void
47+
declare function foo5<T extends string>(a: T, b: NoInfer<{ x: T }>): void
48+
49+
foo1('foo', 'foo') // ok
50+
foo1('foo', 'bar') // error
51+
~~~~~
52+
!!! error TS2345: Argument of type '"bar"' is not assignable to parameter of type '"foo"'.
53+
foo2('foo', ['bar']) // error
54+
~~~~~
55+
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
56+
foo3('foo', ['bar']) // error
57+
~~~~~
58+
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
59+
foo4('foo', { x: 'bar' }) // error
60+
~
61+
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
62+
!!! related TS6500 noInfer.ts:31:52: The expected type comes from property 'x' which is declared here on type '{ x: "foo"; }'
63+
foo5('foo', { x: 'bar' }) // error
64+
~
65+
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
66+
!!! related TS6500 noInfer.ts:32:60: The expected type comes from property 'x' which is declared here on type 'NoInfer<{ x: "foo"; }>'
67+
68+
declare class Animal { move(): void }
69+
declare class Dog extends Animal { woof(): void }
70+
declare function doSomething<T>(value: T, getDefault: () => NoInfer<T>): void;
71+
72+
doSomething(new Animal(), () => new Animal()); // ok
73+
doSomething(new Animal(), () => new Dog()); // ok
74+
doSomething(new Dog(), () => new Animal()); // error
75+
~~~~~~~~~~~~
76+
!!! error TS2741: Property 'woof' is missing in type 'Animal' but required in type 'Dog'.
77+
!!! related TS2728 noInfer.ts:42:36: 'woof' is declared here.
78+
!!! related TS6502 noInfer.ts:43:55: The expected type comes from the return type of this signature.
79+
80+
declare function assertEqual<T>(actual: T, expected: NoInfer<T>): boolean;
81+
82+
assertEqual({ x: 1 }, { x: 3 }); // ok
83+
const g = { x: 3, y: 2 };
84+
assertEqual(g, { x: 3 }); // error
85+
~~~~~~~~
86+
!!! error TS2345: Argument of type '{ x: number; }' is not assignable to parameter of type '{ x: number; y: number; }'.
87+
!!! error TS2345: Property 'y' is missing in type '{ x: number; }' but required in type '{ x: number; y: number; }'.
88+
!!! related TS2728 noInfer.ts:52:19: 'y' is declared here.
89+
90+
declare function invoke<T, R>(func: (value: T) => R, value: NoInfer<T>): R;
91+
declare function test(value: { x: number; }): number;
92+
93+
invoke(test, { x: 1, y: 2 }); // error
94+
~
95+
!!! error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
96+
test({ x: 1, y: 2 }); // error
97+
~
98+
!!! error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
99+
100+
type Component<Props> = { props: Props; };
101+
declare function doWork<Props>(Component: Component<Props>, props: NoInfer<Props>): void;
102+
declare const comp: Component<{ foo: number }>;
103+
104+
doWork(comp, { foo: 42 }); // ok
105+
doWork(comp, {}); // error
106+
~~
107+
!!! error TS2345: Argument of type '{}' is not assignable to parameter of type '{ foo: number; }'.
108+
!!! error TS2345: Property 'foo' is missing in type '{}' but required in type '{ foo: number; }'.
109+
!!! related TS2728 noInfer.ts:63:33: 'foo' is declared here.
110+
111+
declare function mutate<T>(callback: (a: NoInfer<T>, b: number) => T): T;
112+
const mutate1 = mutate((a, b) => b);
113+
114+
declare class ExampleClass<T> {}
115+
class OkClass<T> {
116+
constructor(private clazz: ExampleClass<T>, private _value: NoInfer<T>) {}
117+
118+
get value(): T {
119+
return this._value; // ok
120+
}
121+
}
122+
class OkClass2<T> {
123+
constructor(private clazz: ExampleClass<T>, public _value: NoInfer<T>) {}
124+
}
125+

0 commit comments

Comments
 (0)