-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Changes from 8 commits
ba7f084
411205e
b25bf3e
9438168
e492451
63b6713
14b90c2
2fc975a
af2aae4
79f8513
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
})); | ||
|
||
const SymbolLinks = class implements SymbolLinks { | ||
|
@@ -6710,7 +6712,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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the latest commit this will now emit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a test for that? Same with Uppercase etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't, at least not that I can tell. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test added. |
||
return noInferSymbol ? symbolToTypeNode(noInferSymbol, context, SymbolFlags.Type, [typeNode]) : typeNode; | ||
} | ||
|
||
return Debug.fail("Should be unreachable."); | ||
|
@@ -15866,8 +15870,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!; | ||
|
@@ -16049,10 +16056,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) { | ||
|
@@ -16066,7 +16095,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) { | ||
|
@@ -17740,7 +17769,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) : | ||
|
@@ -19822,6 +19852,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 | ||
|
@@ -25281,7 +25314,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) { | ||
|
@@ -25354,6 +25387,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) { | ||
|
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>) {} | ||
} | ||
|
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. Anintrinsic
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.