Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NoInfer<T> intrinsic represented as special substitution type #56794

Merged
merged 10 commits into from
Jan 12, 2024
53 changes: 44 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1363,13 +1363,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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly related question: should ThisType become an intrinsic type as well (it is kinda intrinsic by nature but it's not defined as such in the typedefs)? Or is it better to not touch it at all?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously this shouldn't be touched as part of this PR - I just take this opportunity to ask a question about it ;p

Copy link
Member Author

@ahejlsberg ahejlsberg Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to leave ThisType as is. An intrinsic declaration doesn't really say anything about the internal representation of the type, and we already have a perfectly good solution for that with the current interface-based declaration.

}));

const SymbolLinks = class implements SymbolLinks {
Expand Down Expand Up @@ -6710,7 +6712,8 @@ 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);
return isNoInferType(type) ? factory.createTypeReferenceNode("NoInfer", [typeNode]) : typeNode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in declaration emit when something like this gets serialized?

// foo.ts
export const f: <T>(x: T, y: NoInfer<T>) => bool;

// bar.ts

import { f } from "./foo.js";

type NoInfer<T> = number;

export const g = f;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the latest commit this will now emit globalThis.NoInfer<T>, similar to what we do for other global types in conflict situations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test for that? Same with Uppercase etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't, at least not that I can tell.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added.

}

return Debug.fail("Should be unreachable.");
Expand Down Expand Up @@ -15866,8 +15869,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 @@ -16049,10 +16055,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 @@ -16066,7 +16094,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 @@ -17731,7 +17759,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function shouldDeferIndexType(type: Type, indexFlags = IndexFlags.None) {
return !!(type.flags & TypeFlags.InstantiableNonPrimitive ||
return !!(type.flags & TypeFlags.InstantiableNonPrimitive && !isNoInferType(type) ||
isGenericTupleType(type) ||
isGenericMappedType(type) && (!hasDistributiveNameType(type) || getMappedTypeNameTypeKind(type) === MappedTypeNameTypeKind.Remapping) ||
type.flags & TypeFlags.Union && !(indexFlags & IndexFlags.NoReducibleCheck) && isGenericReducibleType(type) ||
Expand All @@ -17743,6 +17771,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return 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))) :
type.flags & TypeFlags.Substitution ? getIndexType((type as SubstitutionType).baseType, indexFlags) :
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, indexFlags) :
type === wildcardType ? wildcardType :
type.flags & TypeFlags.Unknown ? neverType :
Expand Down Expand Up @@ -19822,6 +19851,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 @@ -25281,7 +25313,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 @@ -25354,6 +25386,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 @@ -6705,8 +6705,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[]>;

// NoInfer<T> is erased in index types and mapped types

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
Loading