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 intrinsic type #52968

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 56 additions & 23 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ import {
NodeLinks,
nodeStartsNewLexicalEnvironment,
NodeWithTypeArguments,
NoInferType,
NonNullChain,
NonNullExpression,
not,
Expand Down Expand Up @@ -1357,18 +1358,18 @@ const enum MinArgumentCountFlags {
VoidIsNonOptional = 1 << 1,
}

const enum IntrinsicTypeKind {
const enum StringMappingTypeKind {
Uppercase,
Lowercase,
Capitalize,
Uncapitalize,
}

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

const SymbolLinks = class implements SymbolLinks {
Expand Down Expand Up @@ -1940,7 +1941,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var enumLiteralTypes = new Map<string, LiteralType>();
var indexedAccessTypes = new Map<string, IndexedAccessType>();
var templateLiteralTypes = new Map<string, TemplateLiteralType>();
var stringMappingTypes = new Map<string, StringMappingType>();
var intrinsicWrapperTypes = new Map<string, StringMappingType | NoInferType>();
var substitutionTypes = new Map<string, SubstitutionType>();
var subtypeReductionCache = new Map<string, Type[]>();
var decoratorContextOverrideTypeCache = new Map<string, Type>();
Expand Down Expand Up @@ -6699,6 +6700,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const typeNode = typeToTypeNodeHelper((type as StringMappingType).type, context);
return symbolToTypeNode((type as StringMappingType).symbol, context, SymbolFlags.Type, [typeNode]);
}
if (type.flags & TypeFlags.NoInfer) {
const typeNode = typeToTypeNodeHelper((type as NoInferType).type, context);
return symbolToTypeNode((type as NoInferType).symbol, context, SymbolFlags.Type, [typeNode]);
}
if (type.flags & TypeFlags.IndexedAccess) {
const objectTypeNode = typeToTypeNodeHelper((type as IndexedAccessType).objectType, context);
const indexTypeNode = typeToTypeNodeHelper((type as IndexedAccessType).indexType, context);
Expand Down Expand Up @@ -14355,6 +14360,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const constraint = getBaseConstraint((t as StringMappingType).type);
return constraint && constraint !== (t as StringMappingType).type ? getStringMappingType((t as StringMappingType).symbol, constraint) : stringType;
}
if (t.flags & TypeFlags.NoInfer) {
return getBaseConstraint((t as NoInferType).type);
}
if (t.flags & TypeFlags.IndexedAccess) {
if (isMappedTypeGenericIndexedAccess(t)) {
// For indexed access types of the form { [P in K]: E }[X], where K is non-generic and X is generic,
Expand Down Expand Up @@ -15866,8 +15874,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 && typeArguments && typeArguments.length === 1) {
if (stringMappingTypeKinds.has(symbol.escapedName as string)) {
return getStringMappingType(symbol, typeArguments[0]);
}
return getNoInferType(symbol, typeArguments[0]);
}
const links = getSymbolLinks(symbol);
const typeParameters = links.typeParameters!;
Expand Down Expand Up @@ -17884,39 +17895,51 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
type;
}

function getNoInferType(symbol: Symbol, type: Type): Type {
if (!isGenericType(type)) {
return type;
}
const id = `${getSymbolId(symbol)},${getTypeId(type)}`;
let result = intrinsicWrapperTypes.get(id);
if (!result) {
intrinsicWrapperTypes.set(id, result = createNoInferType(symbol, type));
}
return result;
}

function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase:
switch (stringMappingTypeKinds.get(symbol.escapedName as string)) {
case StringMappingTypeKind.Uppercase:
return str.toUpperCase();
case IntrinsicTypeKind.Lowercase:
case StringMappingTypeKind.Lowercase:
return str.toLowerCase();
case IntrinsicTypeKind.Capitalize:
case StringMappingTypeKind.Capitalize:
return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize:
case StringMappingTypeKind.Uncapitalize:
return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}

function applyTemplateStringMapping(symbol: Symbol, texts: readonly string[], types: readonly Type[]): [texts: readonly string[], types: readonly Type[]] {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase:
switch (stringMappingTypeKinds.get(symbol.escapedName as string)) {
case StringMappingTypeKind.Uppercase:
return [texts.map(t => t.toUpperCase()), types.map(t => getStringMappingType(symbol, t))];
case IntrinsicTypeKind.Lowercase:
case StringMappingTypeKind.Lowercase:
return [texts.map(t => t.toLowerCase()), types.map(t => getStringMappingType(symbol, t))];
case IntrinsicTypeKind.Capitalize:
case StringMappingTypeKind.Capitalize:
return [texts[0] === "" ? texts : [texts[0].charAt(0).toUpperCase() + texts[0].slice(1), ...texts.slice(1)], texts[0] === "" ? [getStringMappingType(symbol, types[0]), ...types.slice(1)] : types];
case IntrinsicTypeKind.Uncapitalize:
case StringMappingTypeKind.Uncapitalize:
return [texts[0] === "" ? texts : [texts[0].charAt(0).toLowerCase() + texts[0].slice(1), ...texts.slice(1)], texts[0] === "" ? [getStringMappingType(symbol, types[0]), ...types.slice(1)] : types];
}
return [texts, types];
}

function getStringMappingTypeForGenericType(symbol: Symbol, type: Type): Type {
const id = `${getSymbolId(symbol)},${getTypeId(type)}`;
let result = stringMappingTypes.get(id);
let result = intrinsicWrapperTypes.get(id);
if (!result) {
stringMappingTypes.set(id, result = createStringMappingType(symbol, type));
intrinsicWrapperTypes.set(id, result = createStringMappingType(symbol, type));
}
return result;
}
Expand All @@ -17927,6 +17950,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}

function createNoInferType(symbol: Symbol, type: Type) {
const result = createTypeWithSymbol(TypeFlags.NoInfer, symbol) as NoInferType;
result.type = type;
return result;
}

function createIndexedAccessType(objectType: Type, indexType: Type, accessFlags: AccessFlags, aliasSymbol: Symbol | undefined, aliasTypeArguments: readonly Type[] | undefined) {
const type = createType(TypeFlags.IndexedAccess) as IndexedAccessType;
type.objectType = objectType;
Expand Down Expand Up @@ -19810,6 +19839,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (flags & TypeFlags.StringMapping) {
return getStringMappingType((type as StringMappingType).symbol, instantiateType((type as StringMappingType).type, mapper));
}
if (flags & TypeFlags.NoInfer) {
return getNoInferType((type as NoInferType).symbol, instantiateType((type as NoInferType).type, mapper));
}
if (flags & TypeFlags.IndexedAccess) {
const newAliasSymbol = aliasSymbol || type.aliasSymbol;
const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper);
Expand Down Expand Up @@ -20980,6 +21012,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
type.flags & TypeFlags.UnionOrIntersection ? getNormalizedUnionOrIntersectionType(type as UnionOrIntersectionType, writing) :
type.flags & TypeFlags.Substitution ? writing ? (type as SubstitutionType).baseType : getSubstitutionIntersection(type as SubstitutionType) :
type.flags & TypeFlags.Simplifiable ? getSimplifiedType(type, writing) :
type.flags & TypeFlags.NoInfer ? (type as NoInferType).type :
type;
if (t === type) return t;
type = t;
Expand Down Expand Up @@ -25279,7 +25312,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
inferFromTypes(originalSource, originalTarget);

function inferFromTypes(source: Type, target: Type): void {
if (!couldContainTypeVariables(target)) {
if (!couldContainTypeVariables(target) || source.flags & TypeFlags.NoInfer) {
return;
}
if (source === wildcardType || source === blockedStringType) {
Expand Down Expand Up @@ -44993,7 +45026,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
checkExportsOnMergedDeclarations(node);
checkTypeParameters(node.typeParameters);
if (node.type.kind === SyntaxKind.IntrinsicKeyword) {
if (!intrinsicTypeKinds.has(node.name.escapedText as string) || length(node.typeParameters) !== 1) {
if (!stringMappingTypeKinds.has(node.name.escapedText as string) && (node.name.escapedText as string) !== "NoInfer" || length(node.typeParameters) !== 1) {
error(node.type, Diagnostics.The_intrinsic_keyword_can_only_be_used_to_declare_compiler_provided_intrinsic_types);
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6093,6 +6093,7 @@ export const enum TypeFlags {
NonPrimitive = 1 << 26, // intrinsic object type
TemplateLiteral = 1 << 27, // Template literal type
StringMapping = 1 << 28, // Uppercase/Lowercase type
NoInfer = 1 << 29, // NoInfer type

/** @internal */
AnyOrUnknown = Any | Unknown,
Expand Down Expand Up @@ -6125,7 +6126,7 @@ export const enum TypeFlags {
UnionOrIntersection = Union | Intersection,
StructuredType = Object | Union | Intersection,
TypeVariable = TypeParameter | IndexedAccess,
InstantiableNonPrimitive = TypeVariable | Conditional | Substitution,
InstantiableNonPrimitive = TypeVariable | Conditional | Substitution | NoInfer,
InstantiablePrimitive = Index | TemplateLiteral | StringMapping,
Instantiable = InstantiableNonPrimitive | InstantiablePrimitive,
StructuredOrInstantiable = StructuredType | Instantiable,
Expand Down Expand Up @@ -6688,6 +6689,11 @@ export interface StringMappingType extends InstantiableType {
type: Type;
}

export interface NoInferType extends InstantiableType {
symbol: Symbol;
type: Type;
}

// Type parameter substitution (TypeFlags.Substitution)
// 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
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;

/**
* Blocks the contained type from participating in type inference.
*/
type NoInfer<T> = intrinsic;

/**
* Marker for contextual 'this' type
*/
Expand Down
13 changes: 9 additions & 4 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7107,6 +7107,7 @@ declare namespace ts {
NonPrimitive = 67108864,
TemplateLiteral = 134217728,
StringMapping = 268435456,
NoInfer = 536870912,
Literal = 2944,
Unit = 109472,
Freshable = 2976,
Expand All @@ -7122,11 +7123,11 @@ declare namespace ts {
UnionOrIntersection = 3145728,
StructuredType = 3670016,
TypeVariable = 8650752,
InstantiableNonPrimitive = 58982400,
InstantiableNonPrimitive = 595853312,
InstantiablePrimitive = 406847488,
Instantiable = 465829888,
StructuredOrInstantiable = 469499904,
Narrowable = 536624127,
Instantiable = 1002700800,
StructuredOrInstantiable = 1006370816,
Narrowable = 1073495039,
}
type DestructuringPattern = BindingPattern | ObjectLiteralExpression | ArrayLiteralExpression;
interface Type {
Expand Down Expand Up @@ -7321,6 +7322,10 @@ declare namespace ts {
symbol: Symbol;
type: Type;
}
interface NoInferType extends InstantiableType {
symbol: Symbol;
type: Type;
}
interface SubstitutionType extends InstantiableType {
objectFlags: ObjectFlags;
baseType: Type;
Expand Down
80 changes: 80 additions & 0 deletions tests/baselines/reference/noInfer.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
noInfer.ts(4,12): error TS2345: Argument of type '"bar"' is not assignable to parameter of type '"foo"'.
noInfer.ts(12,30): error TS2741: Property 'woof' is missing in type 'Animal' but required in type 'Dog'.
noInfer.ts(18,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(23,22): error TS2345: Argument of type '{ x: number; y: number; }' is not assignable to parameter of type '{ x: number; }'.
Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
noInfer.ts(24,14): error TS2345: Argument of type '{ x: number; y: number; }' is not assignable to parameter of type '{ x: number; }'.
Object literal may only specify known properties, and 'y' does not exist in type '{ x: number; }'.
noInfer.ts(31,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 (6 errors) ====
export declare function foo<T extends string>(a: T, b: NoInfer<T>): void

foo('foo', 'foo') // ok
foo('foo', 'bar') // error
~~~~~
!!! error TS2345: Argument of type '"bar"' is not assignable to parameter of type '"foo"'.

export declare class Animal { move(): void }
export declare class Dog extends Animal { woof(): void }
export 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:7:43: 'woof' is declared here.
!!! related TS6502 noInfer.ts:8:62: The expected type comes from the return type of this signature.

export 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:17:19: 'y' is declared here.

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

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

export type Component<Props> = { props: Props; };
export declare function doWork<Props>(Component: Component<Props>, props: NoInfer<Props>): void;
export 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:28:40: 'foo' is declared here.

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

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

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

Loading
Loading