Skip to content

Commit

Permalink
Add NoInfer<T> intrinsic represented as special substitution type (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ahejlsberg authored Jan 12, 2024
1 parent fea4a45 commit e6fe96c
Show file tree
Hide file tree
Showing 14 changed files with 1,185 additions and 11 deletions.
54 changes: 45 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1366,13 +1366,15 @@ const enum IntrinsicTypeKind {
Lowercase,
Capitalize,
Uncapitalize,
NoInfer,
}

const intrinsicTypeKinds: ReadonlyMap<string, IntrinsicTypeKind> = new Map(Object.entries({
Uppercase: IntrinsicTypeKind.Uppercase,
Lowercase: IntrinsicTypeKind.Lowercase,
Capitalize: IntrinsicTypeKind.Capitalize,
Uncapitalize: IntrinsicTypeKind.Uncapitalize,
NoInfer: IntrinsicTypeKind.NoInfer,
}));

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

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

function getTypeAliasInstantiation(symbol: Symbol, typeArguments: readonly Type[] | undefined, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
const type = getDeclaredTypeOfSymbol(symbol);
if (type === intrinsicMarkerType && intrinsicTypeKinds.has(symbol.escapedName as string) && typeArguments && typeArguments.length === 1) {
return getStringMappingType(symbol, typeArguments[0]);
if (type === intrinsicMarkerType) {
const typeKind = intrinsicTypeKinds.get(symbol.escapedName as string);
if (typeKind !== undefined && typeArguments && typeArguments.length === 1) {
return typeKind === IntrinsicTypeKind.NoInfer ? getNoInferType(typeArguments[0]) : getStringMappingType(symbol, typeArguments[0]);
}
}
const links = getSymbolLinks(symbol);
const typeParameters = links.typeParameters!;
Expand Down Expand Up @@ -16158,10 +16165,32 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return links.resolvedJSDocType;
}

function getNoInferType(type: Type) {
return isNoInferTargetType(type) ? getOrCreateSubstitutionType(type, unknownType) : type;
}

function isNoInferTargetType(type: Type): boolean {
// This is effectively a more conservative and predictable form of couldContainTypeVariables. We want to
// preserve NoInfer<T> only for types that could contain type variables, but we don't want to exhaustively
// examine all object type members.
return !!(type.flags & TypeFlags.UnionOrIntersection && some((type as UnionOrIntersectionType).types, isNoInferTargetType) ||
type.flags & TypeFlags.Substitution && !isNoInferType(type) && isNoInferTargetType((type as SubstitutionType).baseType) ||
type.flags & TypeFlags.Object && !isEmptyAnonymousObjectType(type) ||
type.flags & (TypeFlags.Instantiable & ~TypeFlags.Substitution) && !isPatternLiteralType(type));
}

function isNoInferType(type: Type) {
// A NoInfer<T> type is represented as a substitution type with a TypeFlags.Unknown constraint.
return !!(type.flags & TypeFlags.Substitution && (type as SubstitutionType).constraint.flags & TypeFlags.Unknown);
}

function getSubstitutionType(baseType: Type, constraint: Type) {
if (constraint.flags & TypeFlags.AnyOrUnknown || constraint === baseType || baseType.flags & TypeFlags.Any) {
return baseType;
}
return constraint.flags & TypeFlags.AnyOrUnknown || constraint === baseType || baseType.flags & TypeFlags.Any ?
baseType :
getOrCreateSubstitutionType(baseType, constraint);
}

function getOrCreateSubstitutionType(baseType: Type, constraint: Type) {
const id = `${getTypeId(baseType)}>${getTypeId(constraint)}`;
const cached = substitutionTypes.get(id);
if (cached) {
Expand All @@ -16175,7 +16204,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getSubstitutionIntersection(substitutionType: SubstitutionType) {
return getIntersectionType([substitutionType.constraint, substitutionType.baseType]);
return isNoInferType(substitutionType) ? substitutionType.baseType : getIntersectionType([substitutionType.constraint, substitutionType.baseType]);
}

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

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

function inferFromTypes(source: Type, target: Type): void {
if (!couldContainTypeVariables(target)) {
if (!couldContainTypeVariables(target) || isNoInferType(target)) {
return;
}
if (source === wildcardType || source === blockedStringType) {
Expand Down Expand Up @@ -25532,6 +25565,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
if (target.flags & (TypeFlags.IndexedAccess | TypeFlags.Substitution)) {
if (isNoInferType(target)) {
return;
}
target = getActualTypeVariable(target);
}
if (target.flags & TypeFlags.TypeVariable) {
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6707,8 +6707,9 @@ export interface StringMappingType extends InstantiableType {
// Substitution types are created for type parameters or indexed access types that occur in the
// true branch of a conditional type. For example, in 'T extends string ? Foo<T> : Bar<T>', the
// reference to T in Foo<T> is resolved as a substitution type that substitutes 'string & T' for T.
// Thus, if Foo has a 'string' constraint on its type parameter, T will satisfy it. Substitution
// types disappear upon instantiation (just like type parameters).
// Thus, if Foo has a 'string' constraint on its type parameter, T will satisfy it.
// Substitution type are also created for NoInfer<T> types. Those are represented as substitution
// types where the constraint is type 'unknown' (which is never generated for the case above).
export interface SubstitutionType extends InstantiableType {
objectFlags: ObjectFlags;
baseType: Type; // Target type
Expand Down
1 change: 1 addition & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,7 @@ export namespace Completion {
typeEntry("Lowercase"),
typeEntry("Capitalize"),
typeEntry("Uncapitalize"),
typeEntry("NoInfer"),
interfaceEntry("ThisType"),
varEntry("ArrayBuffer"),
interfaceEntry("ArrayBufferTypes"),
Expand Down
5 changes: 5 additions & 0 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,11 @@ type Capitalize<S extends string> = intrinsic;
*/
type Uncapitalize<S extends string> = intrinsic;

/**
* Marker for non-inference type position
*/
type NoInfer<T> = intrinsic;

/**
* Marker for contextual 'this' type
*/
Expand Down
125 changes: 125 additions & 0 deletions tests/baselines/reference/noInfer.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
noInfer.ts(35,13): error TS2345: Argument of type '"bar"' is not assignable to parameter of type '"foo"'.
noInfer.ts(36,14): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
noInfer.ts(37,14): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
noInfer.ts(38,15): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
noInfer.ts(39,15): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
noInfer.ts(47,30): error TS2741: Property 'woof' is missing in type 'Animal' but required in type 'Dog'.
noInfer.ts(53,16): error TS2345: Argument of type '{ x: number; }' is not assignable to parameter of type '{ x: number; y: number; }'.
Property 'y' is missing in type '{ x: number; }' but required in type '{ x: number; y: number; }'.
noInfer.ts(58,22): error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
noInfer.ts(59,14): error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
noInfer.ts(66,14): error TS2345: Argument of type '{}' is not assignable to parameter of type '{ foo: number; }'.
Property 'foo' is missing in type '{}' but required in type '{ foo: number; }'.


==== noInfer.ts (10 errors) ====
// NoInfer<T> is erased for primitives

type T00 = NoInfer<string>;
type T01 = NoInfer<string | number | boolean>;
type T02 = NoInfer<undefined>;
type T03 = NoInfer<"foo">;
type T04 = NoInfer<`foo${string}`>;
type T05 = NoInfer<`foo${string}` & `${string}bar`>;
type T06 = NoInfer<{}>;

// NoInfer<T> is preserved for object types

type T10 = NoInfer<string[]>;
type T11 = NoInfer<{ x: string }>;

// NoInfer<T> is erased if it has no effect

type T20<T> = NoInfer<NoInfer<T>>;
type T21<T> = NoInfer<NoInfer<T> & string>;
type T22<T> = NoInfer<NoInfer<T> & string[]>;

// keyof NoInfer<T> is transformed into NoInfer<keyof T>

type T30 = keyof NoInfer<{ a: string, b: string }>;
type T31<T> = keyof NoInfer<T>;
type T32 = { [K in keyof NoInfer<{ a: string, b: string }>]: K };

declare function foo1<T extends string>(a: T, b: NoInfer<T>): void
declare function foo2<T extends string>(a: T, b: NoInfer<T>[]): void
declare function foo3<T extends string>(a: T, b: NoInfer<T[]>): void
declare function foo4<T extends string>(a: T, b: { x: NoInfer<T> }): void
declare function foo5<T extends string>(a: T, b: NoInfer<{ x: T }>): void

foo1('foo', 'foo') // ok
foo1('foo', 'bar') // error
~~~~~
!!! error TS2345: Argument of type '"bar"' is not assignable to parameter of type '"foo"'.
foo2('foo', ['bar']) // error
~~~~~
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
foo3('foo', ['bar']) // error
~~~~~
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
foo4('foo', { x: 'bar' }) // error
~
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
!!! related TS6500 noInfer.ts:31:52: The expected type comes from property 'x' which is declared here on type '{ x: "foo"; }'
foo5('foo', { x: 'bar' }) // error
~
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
!!! related TS6500 noInfer.ts:32:60: The expected type comes from property 'x' which is declared here on type 'NoInfer<{ x: "foo"; }>'

declare class Animal { move(): void }
declare class Dog extends Animal { woof(): void }
declare function doSomething<T>(value: T, getDefault: () => NoInfer<T>): void;

doSomething(new Animal(), () => new Animal()); // ok
doSomething(new Animal(), () => new Dog()); // ok
doSomething(new Dog(), () => new Animal()); // error
~~~~~~~~~~~~
!!! error TS2741: Property 'woof' is missing in type 'Animal' but required in type 'Dog'.
!!! related TS2728 noInfer.ts:42:36: 'woof' is declared here.
!!! related TS6502 noInfer.ts:43:55: The expected type comes from the return type of this signature.

declare function assertEqual<T>(actual: T, expected: NoInfer<T>): boolean;

assertEqual({ x: 1 }, { x: 3 }); // ok
const g = { x: 3, y: 2 };
assertEqual(g, { x: 3 }); // error
~~~~~~~~
!!! error TS2345: Argument of type '{ x: number; }' is not assignable to parameter of type '{ x: number; y: number; }'.
!!! error TS2345: Property 'y' is missing in type '{ x: number; }' but required in type '{ x: number; y: number; }'.
!!! related TS2728 noInfer.ts:52:19: 'y' is declared here.

declare function invoke<T, R>(func: (value: T) => R, value: NoInfer<T>): R;
declare function test(value: { x: number; }): number;

invoke(test, { x: 1, y: 2 }); // error
~
!!! error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
test({ x: 1, y: 2 }); // error
~
!!! error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.

type Component<Props> = { props: Props; };
declare function doWork<Props>(Component: Component<Props>, props: NoInfer<Props>): void;
declare const comp: Component<{ foo: number }>;

doWork(comp, { foo: 42 }); // ok
doWork(comp, {}); // error
~~
!!! error TS2345: Argument of type '{}' is not assignable to parameter of type '{ foo: number; }'.
!!! error TS2345: Property 'foo' is missing in type '{}' but required in type '{ foo: number; }'.
!!! related TS2728 noInfer.ts:63:33: 'foo' is declared here.

declare function mutate<T>(callback: (a: NoInfer<T>, b: number) => T): T;
const mutate1 = mutate((a, b) => b);

declare class ExampleClass<T> {}
class OkClass<T> {
constructor(private clazz: ExampleClass<T>, private _value: NoInfer<T>) {}

get value(): T {
return this._value; // ok
}
}
class OkClass2<T> {
constructor(private clazz: ExampleClass<T>, public _value: NoInfer<T>) {}
}

Loading

0 comments on commit e6fe96c

Please sign in to comment.