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

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Dec 15, 2023

This PR adds official support for a NoInfer<T> marker type that blocks inferences to the contained type. The PR supersedes #52968, but borrows as much as possible from that PR (lots of thanks for your work, @Andarist).

The PR represents NoInfer<T> types as special substitution types (types with kind TypeFlags.Substitution) having constraint type unknown, a pattern that otherwise never occurs. This representation ensures that we maximally leverage our existing infrastructure for erasing marker types when appropriate, and that we minimally disrupt the tooling ecosystem (because we're not introducing a new kind of type).

Other than blocking inference, NoInfer<T> markers have no effect on T. Indeed, T and NoInfer<T> are considered identical types in all other contexts.

Some examples:

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
foo2('foo', ['bar']) // error
foo3('foo', ['bar']) // error
foo4('foo', { x: 'bar' }) // error
foo5('foo', { x: 'bar' }) // error

Above, inferences to the type of b are blocked by the NoInfer<T> marker, thus causing errors to be reported when the argument types differ. Without the NoInfer<T> markers, inference simply produces the union "foo" | "bar".

Fixes #14829.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Dec 15, 2023
@ahejlsberg ahejlsberg added this to the TypeScript 5.4.0 milestone Dec 15, 2023
@andrewbranch
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2023

Heya @andrewbranch, I've started to run the tarball bundle task on this PR at b25bf3e. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2023

Hey @andrewbranch, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/159118/artifacts?artifactName=tgz&fileId=5E32343F3417330FFC1CCF4A8C1947A145A2ECCA7D11F2B39089E255FAC22AD202&fileName=/typescript-5.4.0-insiders.20231215.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

}

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.

@ahejlsberg
Copy link
Member Author

@typescript-bot test top100
@typescript-bot user test this
@typescript-bot run dt
@typescript-bot perf test this faster

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2023

Heya @ahejlsberg, I've started to run the tsc-only perf test suite on this PR at b25bf3e. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2023

Heya @ahejlsberg, I've started to run the diff-based user code test suite on this PR at b25bf3e. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2023

Heya @ahejlsberg, I've started to run the diff-based top-repos suite on this PR at b25bf3e. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2023

Heya @ahejlsberg, I've started to run the parallelized Definitely Typed test suite on this PR at b25bf3e. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

@ahejlsberg
The results of the perf run you requested are in!

Here they are:

Compiler

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Angular - node (v18.15.0, x64)
Memory used 295,417k (± 0.01%) 295,473k (± 0.01%) +55k (+ 0.02%) 295,421k 295,503k p=0.013 n=6
Parse Time 2.65s (± 0.19%) 2.65s (± 0.21%) ~ 2.64s 2.65s p=0.640 n=6
Bind Time 0.82s (± 0.00%) 0.83s (± 0.62%) +0.01s (+ 0.81%) 0.82s 0.83s p=0.025 n=6
Check Time 8.14s (± 0.33%) 8.13s (± 0.18%) ~ 8.12s 8.16s p=0.410 n=6
Emit Time 7.11s (± 0.35%) 7.11s (± 0.23%) ~ 7.09s 7.14s p=0.505 n=6
Total Time 18.72s (± 0.19%) 18.71s (± 0.16%) ~ 18.68s 18.77s p=0.629 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 194,424k (± 1.62%) 193,481k (± 1.56%) ~ 191,529k 197,409k p=0.378 n=6
Parse Time 1.34s (± 1.13%) 1.35s (± 0.00%) ~ 1.35s 1.35s p=0.293 n=6
Bind Time 0.72s (± 0.00%) 0.72s (± 0.00%) ~ 0.72s 0.72s p=1.000 n=6
Check Time 9.25s (± 0.30%) 9.26s (± 0.35%) ~ 9.22s 9.29s p=0.465 n=6
Emit Time 2.63s (± 0.56%) 2.63s (± 0.31%) ~ 2.62s 2.64s p=0.666 n=6
Total Time 13.95s (± 0.30%) 13.96s (± 0.18%) ~ 13.93s 13.99s p=0.685 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,392k (± 0.01%) 347,390k (± 0.00%) ~ 347,375k 347,408k p=1.000 n=6
Parse Time 2.46s (± 0.60%) 2.46s (± 0.33%) ~ 2.45s 2.47s p=1.000 n=6
Bind Time 0.92s (± 0.90%) 0.93s (± 0.44%) ~ 0.92s 0.93s p=0.285 n=6
Check Time 6.90s (± 0.21%) 6.88s (± 0.39%) ~ 6.86s 6.93s p=0.221 n=6
Emit Time 4.06s (± 0.20%) 4.06s (± 0.56%) ~ 4.03s 4.08s p=1.000 n=6
Total Time 14.34s (± 0.17%) 14.33s (± 0.26%) ~ 14.28s 14.38s p=0.517 n=6
TFS - node (v18.15.0, x64)
Memory used 302,658k (± 0.00%) 302,669k (± 0.00%) ~ 302,657k 302,678k p=0.108 n=6
Parse Time 2.00s (± 0.71%) 2.00s (± 1.03%) ~ 1.96s 2.02s p=1.000 n=6
Bind Time 1.00s (± 1.17%) 1.01s (± 0.97%) ~ 1.00s 1.02s p=0.066 n=6
Check Time 6.28s (± 0.25%) 6.27s (± 0.41%) ~ 6.23s 6.29s p=0.569 n=6
Emit Time 3.59s (± 0.48%) 3.58s (± 0.34%) ~ 3.56s 3.59s p=0.548 n=6
Total Time 12.86s (± 0.28%) 12.85s (± 0.15%) ~ 12.82s 12.87s p=0.744 n=6
material-ui - node (v18.15.0, x64)
Memory used 506,811k (± 0.01%) 506,831k (± 0.00%) ~ 506,819k 506,836k p=0.127 n=6
Parse Time 2.59s (± 0.62%) 2.58s (± 0.77%) ~ 2.54s 2.60s p=0.328 n=6
Bind Time 0.98s (± 1.06%) 1.00s (± 0.52%) ~ 0.99s 1.00s p=0.051 n=6
Check Time 16.89s (± 0.21%) 17.00s (± 0.55%) ~ 16.89s 17.10s p=0.065 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.46s (± 0.22%) 20.57s (± 0.46%) ~ 20.47s 20.67s p=0.063 n=6
xstate - node (v18.15.0, x64)
Memory used 512,794k (± 0.01%) 512,848k (± 0.01%) ~ 512,788k 512,916k p=0.066 n=6
Parse Time 3.27s (± 0.32%) 3.27s (± 0.16%) ~ 3.27s 3.28s p=0.242 n=6
Bind Time 1.54s (± 0.53%) 1.54s (± 0.36%) ~ 1.53s 1.54s p=0.859 n=6
Check Time 2.82s (± 0.27%) 2.84s (± 0.50%) +0.02s (+ 0.77%) 2.82s 2.86s p=0.009 n=6
Emit Time 0.07s (± 0.00%) 0.08s (± 9.21%) 🔻+0.01s (+16.67%) 0.07s 0.09s p=0.009 n=6
Total Time 7.69s (± 0.13%) 7.73s (± 0.28%) +0.04s (+ 0.52%) 7.71s 7.76s p=0.005 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Angular - node (v18.15.0, x64)
  • Compiler-Unions - node (v18.15.0, x64)
  • Monaco - node (v18.15.0, x64)
  • TFS - node (v18.15.0, x64)
  • material-ui - node (v18.15.0, x64)
  • xstate - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

@ahejlsberg Here are the results of running the user test suite comparing main and refs/pull/56794/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

puppeteer

packages/browsers/test/src/tsconfig.json

@typescript-bot
Copy link
Collaborator

Hey @ahejlsberg, the results of running the DT tests are ready.
Everything looks the same!
You can check the log here.

@fatcerberus
Copy link

Without the NoInfer<T> markers, inference simply produces the union "foo" | "bar".

Huh, so it does... here I had thought TS never inferred cross-candidate unions, but apparently there are cases when it does?

@RyanCavanaugh
Copy link
Member

Unions can be formed under the same constrained primitive family. That's why my TS Congress talk had to disavow knowledge of primitive types.

@typescript-bot
Copy link
Collaborator

@ahejlsberg Here are the results of running the top-repos suite comparing main and refs/pull/56794/merge:

Everything looks good!

@ahejlsberg
Copy link
Member Author

Tests and performance all look unaffected.

@craigphicks
Copy link

craigphicks commented Dec 17, 2023

Some examples:

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
foo2('foo', ['bar']) // error
foo3('foo', ['bar']) // error
foo4('foo', { x: 'bar' }) // error
foo5('foo', { x: 'bar' }) // error

All these cases can be addressed by using extends as follows:

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

foo1('foo', 'foo') // ok
foo1('foo', 'bar') // error
foo2('foo', ['bar']) // error
foo3('foo', ['bar']) // error
foo4('foo', { x: 'bar' }) // error
foo5('foo', { x: 'bar' }) // error


declare function invoke<F extends ((value:any) => any)>(func:F, value: Parameters<F>["0"]): ReturnType<F>;
declare function test(value: { x: number; }): number;
invoke(test, { x: 1, y: 2 }); // Compiler Error
test({ x: 1, y: 2 }); // Same Compiler error


playground

It is not a trick. Do you have any stronger cases?

I would not call those test cases buggy. With only that evidence it seems like perhaps this pull is a performance optimization, because you are explicitly shutting down inference, which could only shorten processing time, which is not a bad thing at all.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2023

Hey @DanielRosenwasser, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/159134/artifacts?artifactName=tgz&fileId=B8324752B810A78A78AD8A90EDD001B4CF0393014CE0810C88CFCE3F0DEB2DF202&fileName=/typescript-5.4.0-insiders.20231218.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

@@ -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.

@craigphicks
Copy link

craigphicks commented Dec 18, 2023

My gut feeling is that the infer vs noinfer attribute belongs to the call, rather than to the parameter types of the call.

I have to explain what I mean by "belongs to the call".

You might have a multiple parameter types that appears in multiple calls:

type X<T> {
    id: "x"
    a: T
    b: (x: X<T>): T
};

type Y<T> {
    id: "y"
    a: T
    b: (x: X<T>): T
};

type X<T> = {
    id?: "x"
    a: T
    b: X<T>;
};

type Y<T> = {
    id?: "y"
    a: T
    b: Y<T>
};

declare function fInferFromMemberA0<U, T extends U, Z extends X<U>|Y<U>>(z:Z): (x: T)=>Z;
declare function fInferFromMemberB0<U, T extends U, Z extends X<U>|Y<U>>(z:Z): (x: T)=>Z;

const ta01 = fInferFromMemberA0({a:1, b:0 as any as X<"">});
// does not infer typeof Z.a, but infers type of Z
//const ta01: (x: unknown) => {    
//     a: number;
//     b: X<"">;
// }
const ta02 = fInferFromMemberB0({a:1, b:0 as any as X<"">})
// does not infer type arg of typeof Z.b, but infers type of Z
// const ta02: (x: unknown) => {
//     a: number;
//     b: X<"">;
// }

declare function fInferFromMemberA1<U, T extends U, Z extends X<U>|Y<U> >(z:Z & {a:T}): (x: T)=>Z;
declare function fInferFromMemberB1<U, T extends U, Z extends X<U>|Y<U> >(z:Z & {b:X<T>|Y<T>}): (x: T)=>Z;

const ta11 = fInferFromMemberA1({a:1, b:0 as any as X<"">});
// infers both typeof Z.a, and type of Z
// const ta11: (x: number) => {
//     a: number;
//     b: X<"">;
// }
const ta12 = fInferFromMemberB1({a:1, b:0 as any as X<"">})
// infers both type arg of typeof Z.b, and type of Z
// const ta12: (x: "") => {
//     a: number;
//     b: X<"">;
// }

Trying to infer typeof Z.a in fInferFromMemberA0 does not work (regardless of NoInfer usage), although it could infer Z.

In order to infer typeof Z.a in fInferFromMemberA1, I had to add & {a:T} to function argument type (z:Z & {a:T}). Then it works correctly, and NoInfer wasn't used.

Switching to member Z.b works similarly.

The ability to reach into type structure to infer type from specified members could be useful, but NoInfer types do not make a difference to what is currently allowed. (I tried).

And if it did, the choice of which member to infer from is likely depend upon the function, not the parameter type passed.

@craigphicks
Copy link

craigphicks commented Dec 19, 2023

Cases from 2.8 release documentation + NoInfer

type Foo<T> = T extends { a: infer U; b: NoInfer<infer U> } ? U : never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // never (expecting string)
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: NoInfer< infer U>) => void }
  ? U
  : never;
type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // never (expecting string)

Try with predefined type

type X<U> = { a: U; b: NoInfer<U> };
type Foo<T> = T extends X<infer U> ? U : never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // never (expecting string)
type Y<U> = { a: (x: U) => void; b: (x: NoInfer<U>) => void }
type Bar<T> = T extends Y<infer U> ? U : never;
type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // never (expecting string)

@ahejlsberg
Copy link
Member Author

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 20, 2023

Heya @ahejlsberg, I've started to run the tarball bundle task on this PR at 2fc975a. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 20, 2023

Hey @ahejlsberg, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/159140/artifacts?artifactName=tgz&fileId=3D23A0C817DD7FA525E171E302DDA2643043A9E12BDAFB80A686389EE0054E4E02&fileName=/typescript-5.4.0-insiders.20231220.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

@Xample
Copy link

Xample commented Feb 20, 2024

I'm surprised not to see more examples where the type is infered from the returned value. Here is a (simplified) real world example of a code I faced where it was unpossible to catch errors without the Noinfer keyworld :

// TS <5.4

function getValue<T = string>(): T {
  return undefined as T;
}

function getNumberInline(): number {
  return getValue(); // no error, T in inferred as number (weird behavior)
}

function getNumberUsingVariable(): number{

const value = getValue(); // error, T takes the default string value
  return value;
}

// TS >=5.4

function getValueNoInfer<T = string>(): NoInfer<T> {
  return undefined as T;
}

function getNumberInlineNoInfer(): number {
  return getValueNoInfer(); // error, T takes the default string value
}

function getNumberUsingVariableNoInfer(): number{

const value = getValueNoInfer(); // error, T takes the default string value
  return value;
}

@dhmk083
Copy link

dhmk083 commented Feb 24, 2024

It would be nice if the following pattern could work:

declare function create<T>(fn: (get: () => NoInfer<T>) => T): T

create(get => ({
    id: 123,
    getId() {
        return get().id
    }
}))

Right now T is inferred as unknown.

If I understand NoInfer<T> correctly, it should give a hint to a compiler to not try to infer get-parameter type and instead infer T from type of returned object.

@Xample
Copy link

Xample commented Feb 24, 2024

@dhmk083 shouldn't you write:

declare function create<T>(fn: (get: () => NoInfer<T>) => T): T

const get = ()=> ({
    id: 123,
    getId() {
        return get().id
    }
})

create(get)

@dhmk083
Copy link

dhmk083 commented Feb 24, 2024

No, it was just a simplified example. Here you can read about real world usage. (Check the first collapsible section Why can't we simply infer the type from the initial state?, I don't know how to link to it directly.)

@Andarist
Copy link
Contributor

Andarist commented Feb 27, 2024

@dhmk083 currently, TS can't defer this sufficiently enough, it has to assign the contextual type for get before it even looks into your function. To do that it instantiates the type of this parameter and all of the type parameters in its scope (and T is its outer type parameter).

It's probably not impossible to improve this by deferring the instantiation of those parameter types that are only observable by the return types. I don't feel that it's particularly related to NoInfer though.

And note that ur getId is circular (its return type depends on T) - so even if the type parameters used by return types would get deferred, it likely still wouldn't fix your problem entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Suggestion: Noninferential type parameter usage
10 participants