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

Infer unknown at value positions of objects inferred from keyof T #55547

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Andarist
Copy link
Contributor

@Andarist Andarist commented Aug 28, 2023

I noticed this when investigating a completion problem. Using any here might "accidentally leak" (which happens in the changed baselines) without the user noticing. I think it would be great to change this to unknown unless there is a strong reason why it should stay this way.

Note that this function was introduced by @sandersn in #19227 (and any was used in its followup: #21271 ). This was in pre(historic)-3.0 times when unknown wasn't even a thing in the language yet 😉 So back then unknown wasn't even an option.

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 28, 2023
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@weswigham weswigham added the Breaking Change Would introduce errors in existing code label Sep 13, 2023
@weswigham
Copy link
Member

@DanielRosenwasser I think this is a pretty good break - should we just pull it in now, or should we wait until a different part of the release cycle?

@DanielRosenwasser
Copy link
Member

@typescript-bot pack this
@typescript-bot test this
@typescript-bot test top300
@typescript-bot user test this
@typescript-bot run dt
@typescript-bot perf test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 13, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 13, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 13, 2023

Heya @DanielRosenwasser, I've started to run the regular perf test suite on this PR at fa43c1c. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 13, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 13, 2023

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 13, 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/157726/artifacts?artifactName=tgz&fileId=37F0E40293B7F8B086C57F1269A937169BBA9E52B8135A82BB41191FEEFDE0F802&fileName=/typescript-5.3.0-insiders.20230913.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]".;

@typescript-bot
Copy link
Collaborator

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

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Unknown failure"
  • 2 instances of "Package install failed"

Otherwise...

Everything looks good!

@typescript-bot
Copy link
Collaborator

@DanielRosenwasser
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 (v16.17.1, x64)
Memory used 300,531k (± 0.01%) 300,541k (± 0.01%) ~ 300,511k 300,587k p=0.810 n=6
Parse Time 3.01s (± 0.17%) 3.01s (± 0.28%) ~ 3.00s 3.02s p=0.533 n=6
Bind Time 0.93s (± 0.00%) 0.93s (± 0.00%) ~ 0.93s 0.93s p=1.000 n=6
Check Time 9.34s (± 0.23%) 9.33s (± 0.13%) ~ 9.32s 9.35s p=1.000 n=6
Emit Time 7.63s (± 0.33%) 7.62s (± 0.30%) ~ 7.60s 7.66s p=0.467 n=6
Total Time 20.90s (± 0.10%) 20.89s (± 0.17%) ~ 20.86s 20.96s p=0.195 n=6
Compiler-Unions - node (v16.17.1, x64)
Memory used 194,223k (± 0.01%) 194,257k (± 0.02%) +35k (+ 0.02%) 194,227k 194,293k p=0.020 n=6
Parse Time 1.58s (± 0.00%) 1.58s (± 0.26%) ~ 1.57s 1.58s p=0.405 n=6
Bind Time 0.80s (± 0.65%) 0.80s (± 0.51%) ~ 0.79s 0.80s p=0.595 n=6
Check Time 9.94s (± 0.22%) 9.96s (± 0.66%) ~ 9.89s 10.06s p=1.000 n=6
Emit Time 2.73s (± 0.31%) 2.74s (± 0.23%) ~ 2.73s 2.75s p=0.226 n=6
Total Time 15.05s (± 0.18%) 15.07s (± 0.42%) ~ 15.01s 15.17s p=0.872 n=6
Monaco - node (v16.17.1, x64)
Memory used 347,282k (± 0.01%) 347,296k (± 0.01%) ~ 347,247k 347,320k p=0.574 n=6
Parse Time 2.69s (± 0.28%) 2.69s (± 0.43%) ~ 2.68s 2.71s p=0.796 n=6
Bind Time 0.99s (± 0.00%) 0.99s (± 0.00%) ~ 0.99s 0.99s p=1.000 n=6
Check Time 7.95s (± 0.22%) 7.94s (± 0.29%) ~ 7.91s 7.97s p=0.372 n=6
Emit Time 4.26s (± 0.25%) 4.26s (± 0.41%) ~ 4.23s 4.28s p=0.413 n=6
Total Time 15.90s (± 0.12%) 15.87s (± 0.27%) ~ 15.83s 15.94s p=0.370 n=6
TFS - node (v16.17.1, x64)
Memory used 301,400k (± 0.00%) 301,405k (± 0.00%) ~ 301,396k 301,417k p=0.332 n=6
Parse Time 2.18s (± 0.65%) 2.18s (± 0.63%) ~ 2.16s 2.20s p=0.680 n=6
Bind Time 1.11s (± 0.37%) 1.11s (± 0.57%) ~ 1.10s 1.12s p=0.673 n=6
Check Time 7.24s (± 0.23%) 7.23s (± 0.25%) ~ 7.21s 7.26s p=0.281 n=6
Emit Time 3.98s (± 0.16%) 3.98s (± 0.51%) ~ 3.95s 4.01s p=0.737 n=6
Total Time 14.51s (± 0.16%) 14.50s (± 0.26%) ~ 14.44s 14.54s p=1.000 n=6
material-ui - node (v16.17.1, x64)
Memory used 479,873k (± 0.00%) 479,885k (± 0.00%) ~ 479,866k 479,906k p=0.172 n=6
Parse Time 3.15s (± 0.00%) 3.15s (± 0.40%) ~ 3.13s 3.16s p=0.652 n=6
Bind Time 0.91s (± 0.00%) 0.91s (± 0.00%) ~ 0.91s 0.91s p=1.000 n=6
Check Time 17.84s (± 0.42%) 17.87s (± 0.48%) ~ 17.81s 18.04s p=0.628 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 21.91s (± 0.33%) 21.94s (± 0.40%) ~ 21.87s 22.10s p=0.687 n=6
xstate - node (v16.17.1, x64)
Memory used 542,824k (± 0.01%) 542,905k (± 0.02%) ~ 542,812k 543,081k p=0.230 n=6
Parse Time 3.69s (± 0.22%) 3.69s (± 0.28%) ~ 3.68s 3.71s p=0.270 n=6
Bind Time 1.40s (± 4.71%) 1.38s (± 4.12%) ~ 1.34s 1.45s p=0.418 n=6
Check Time 3.25s (± 2.59%) 3.27s (± 2.71%) ~ 3.15s 3.34s p=0.564 n=6
Emit Time 0.08s (± 0.00%) 0.08s (± 0.00%) ~ 0.08s 0.08s p=1.000 n=6
Total Time 8.42s (± 0.33%) 8.43s (± 0.57%) ~ 8.36s 8.48s p=0.572 n=6
System info unknown
Hosts
  • node (v16.17.1, x64)
Scenarios
  • Angular - node (v16.17.1, x64)
  • Compiler-Unions - node (v16.17.1, x64)
  • Monaco - node (v16.17.1, x64)
  • TFS - node (v16.17.1, x64)
  • material-ui - node (v16.17.1, x64)
  • xstate - node (v16.17.1, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

tsserver

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsTSServer - node (v16.17.1, x64)
Req 1 - updateOpen 2,490ms (± 0.14%) 2,492ms (± 0.11%) ~ 2,488ms 2,495ms p=0.517 n=6
Req 2 - geterr 5,935ms (± 0.45%) 5,944ms (± 0.21%) ~ 5,929ms 5,960ms p=0.261 n=6
Req 3 - references 344ms (± 0.35%) 341ms (± 0.50%) -3ms (- 0.82%) 339ms 343ms p=0.018 n=6
Req 4 - navto 276ms (± 0.78%) 277ms (± 0.93%) ~ 275ms 280ms p=0.242 n=6
Req 5 - completionInfo count 1,356 (± 0.00%) 1,356 (± 0.00%) ~ 1,356 1,356 p=1.000 n=6
Req 5 - completionInfo 82ms (± 7.22%) 86ms (± 6.45%) ~ 82ms 94ms p=0.072 n=6
CompilerTSServer - node (v16.17.1, x64)
Req 1 - updateOpen 2,626ms (± 0.46%) 2,624ms (± 0.58%) ~ 2,605ms 2,640ms p=1.000 n=6
Req 2 - geterr 4,782ms (± 0.15%) 4,782ms (± 0.19%) ~ 4,773ms 4,799ms p=0.872 n=6
Req 3 - references 350ms (± 0.36%) 351ms (± 0.33%) ~ 350ms 353ms p=0.498 n=6
Req 4 - navto 269ms (± 0.31%) 271ms (± 1.35%) ~ 268ms 278ms p=0.122 n=6
Req 5 - completionInfo count 1,518 (± 0.00%) 1,518 (± 0.00%) ~ 1,518 1,518 p=1.000 n=6
Req 5 - completionInfo 79ms (± 0.52%) 79ms (± 0.70%) ~ 78ms 79ms p=0.054 n=6
xstateTSServer - node (v16.17.1, x64)
Req 1 - updateOpen 2,706ms (± 0.23%) 2,708ms (± 0.20%) ~ 2,702ms 2,717ms p=0.572 n=6
Req 2 - geterr 1,948ms (± 2.37%) 1,976ms (± 1.68%) ~ 1,912ms 2,004ms p=0.109 n=6
Req 3 - references 133ms (± 7.82%) 134ms (± 8.17%) ~ 113ms 142ms p=0.469 n=6
Req 4 - navto 357ms (± 0.65%) 358ms (± 0.60%) ~ 355ms 361ms p=0.419 n=6
Req 5 - completionInfo count 2,071 (± 0.00%) 2,071 (± 0.00%) ~ 2,071 2,071 p=1.000 n=6
Req 5 - completionInfo 323ms (± 1.74%) 326ms (± 1.99%) ~ 319ms 333ms p=0.260 n=6
System info unknown
Hosts
  • node (v16.17.1, x64)
Scenarios
  • CompilerTSServer - node (v16.17.1, x64)
  • Compiler-UnionsTSServer - node (v16.17.1, x64)
  • xstateTSServer - node (v16.17.1, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
tsc-startup - node (v16.17.1, x64)
Execution time 156.50ms (± 0.16%) 156.71ms (± 0.19%) +0.20ms (+ 0.13%) 154.96ms 161.09ms p=0.000 n=600
tsserver-startup - node (v16.17.1, x64)
Execution time 230.71ms (± 0.13%) 231.50ms (± 0.15%) +0.79ms (+ 0.34%) 229.78ms 238.98ms p=0.000 n=600
tsserverlibrary-startup - node (v16.17.1, x64)
Execution time 235.61ms (± 0.13%) 236.20ms (± 0.11%) +0.59ms (+ 0.25%) 235.09ms 240.86ms p=0.000 n=600
typescript-startup - node (v16.17.1, x64)
Execution time 235.73ms (± 0.12%) 235.71ms (± 0.14%) ~ 234.32ms 240.10ms p=0.060 n=600
System info unknown
Hosts
  • node (v16.17.1, x64)
Scenarios
  • tsc-startup - node (v16.17.1, x64)
  • tsserver-startup - node (v16.17.1, x64)
  • tsserverlibrary-startup - node (v16.17.1, x64)
  • typescript-startup - node (v16.17.1, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

Hey @DanielRosenwasser, it looks like the DT test run failed. Please check the log for more details.
You can check the log here.

@typescript-bot
Copy link
Collaborator

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

Something interesting changed - please have a look.

Details

vuejs/core

1 of 3 projects failed to build with the old tsc and were ignored

tsconfig.json

@Andarist
Copy link
Contributor Author

A minimal repro case of the above problem: TS playground. It looks fixable on the Vue's side. I think that it actually somewhat proves how this is an improvement because the return type of this function utilizes the inferred type of Props. So even though this setup function tries to constraint Props to { msg: T } where T extends string the inferred { msg: any } leaks~ and the resulting function is callable with incompatible prop types.

@sandersn
Copy link
Member

@DanielRosenwasser did we discuss this at the design meeting yet? I don't see a link to any particular meeting.

@DanielRosenwasser
Copy link
Member

Don't think we did - we probably should.

@sandersn
Copy link
Member

@weswigham would you be able to talk about it at the Friday meeting?

@DanielRosenwasser
Copy link
Member

I guess the only thing I might say is that we could change this behavior based on noImplicitAny, but tests do come up clean.

@weswigham
Copy link
Member

Honestly, I kinda just wanna ship it - any breaks that this causes are almost certainly good breaks where you've done something very unsafe, likely unintentionally, since inference is oft inscrutable, and it's not always obvious your dominating inference source is a keyof T position.

@sandersn
Copy link
Member

sandersn commented Dec 1, 2023

That's fine with me if we have a plan to fix Vue.

@Andarist
Copy link
Contributor Author

Andarist commented Dec 1, 2023

That's fine with me if we have a plan to fix Vue.

I will prioritize investigating this.

@Andarist
Copy link
Contributor Author

Andarist commented Jan 5, 2024

I dug into this case deeper and I think there are two things that could be done about this and both are quite independent from this. They could become prerequisites for this PR.

One thing about the example above is that a pretty low priority inference wins over something better that could be inferred from the generic signature. I’ve opened this issue about it: #56931 and an experimental fix for it: #56939 . The extended community test suite didn’t fail there for Vue so likely the combination of that PR and this PR here wouldnt either.

The second issue is that by inferring an object with properties typed as unknown the inference might fail the constraint check. T could be constrained to Record<string, number> (or something else).

Now… is that a problem? Inferring anything here is quite unsound unless there are some other things at play (like a return context or smth). unknown is the safest option because it assumes that the function might create an object with those properties in its implementation (at least when those keys are passed at argument position). We can’t safely say anything about its values.

Going further… if a union of keys is passed not within an array or something… we can’t even say that those properties are required. If they are passed within an array we similarly can’t guarantee that this array contain all of the keys that we can see in the type system. So really we can only say that all properties are optional.

The question is - what are the use cases for this inference? Why it was introduced? Are those use cases covered today by more sound alternatives?

OTOH, similar unsound situations can always be achieved when supplying type arguments manually (and likely in other situations too). So perhaps the mental model is that it’s not the type system’s responsibility to ensure soundness here but rather the function/signature’s author.

If the type system is meant to allow this regardless of constraints then unknown is not the best fit here. Note that the potential constraint failure is not a new problem:

declare function test1<T>(keys: keyof T): T;
const result1 = test1("" as "a" | "b"); // { a: any; b: any; }

declare function test2<T extends Record<string, never>>(keys: keyof T): T;
const result2 = test2("" as "a" | "b"); // Record<string, never>

This case is far-fetched but conceptually the point stands - the better fit would be the constraint of each potential property.

As @weswigham noted here, it’s not enough to call getBaseConstraintOfType in a situation like this because it wouldn't properly work with constraints depending on other type parameters (like Record<string, U>). Since this fake object created based on the keys is created during the main inference pass, it might be enough to obtain getConstraintOfType, leave U in it and hope that it gets instantiated down the road.

I'm not sure if that would actually happen though (I will try after we establish here how this whole thing should work). If not then I'd say that this could utilize the reverse typed machinery. Instead of creating an anonymous object here, we could create a reverse mapped type (with a fake source object) and depend on the fix from #56300 to get correct (and individual) constraints for each property.

@jfet97
Copy link
Contributor

jfet97 commented Jan 5, 2024

One thing about the example above is that a pretty low priority inference wins over something better that could be inferred from the generic signature. I’ve opened this issue about it: #56931 and an experimental fix for it: #56939 . The extended community test suite didn’t fail there for Vue so likely the combination of that PR and this PR here wouldnt either.

@Andarist I tested your minimal repro with #56939 (here is the playground):

declare function defineComponent<Props extends Record<string, any>>(
  setup: (props: Props) => void,
  options?: {
    props?: (keyof Props)[];
  },
): (props: Props) => any;

defineComponent(
  <T extends string>(_props: { msg: T }) => {
    return () => {};
  },
  {
    props: ["msg"],
  },
);

The inferred type of the call to defineComponent is:

function defineComponent<{
    msg: T;
}>(setup: (props: {
    msg: T;
}) => void, options?: {
    props?: "msg"[] | undefined;
} | undefined): <T extends string>(props: {
    msg: T;
}) => any

Therefore the Props type parameter of defineComponent has been unified with { msg: T }, where T is the type parameter of the passed setup callback. Seems good to me :D

@jfet97
Copy link
Contributor

jfet97 commented Jan 5, 2024

@Andarist
Thinking about it better though there might be a problem. With #56939 the following one would be allowed, but it shouldn't because test has not been explicitly declared as runtime prop:

defineComponent(
  <T extends string>(_props: { msg: T, test: T }) => {
    return () => {};
  },
  {
    props: ["msg"],
  },
);

If you see their documentation:
image

it seems that they want to force you to manually declare the props inside the array before allowing you to use them as arguments of the setup function. In fact, currently, the snippet above which tries to destructure test produces an error.

Therefore it appears that their desired behavior is for the props array to have a higher priority.

@jfet97
Copy link
Contributor

jfet97 commented Jan 5, 2024

@Andarist Thanks to #55811 , if #56939 lands, they could use a definition similar to the following one to error out when someone destructures a property not declared in the props array:

declare function defineComponent<
  Props extends Record<string, any>,
  const RuntimeProps extends readonly string[]
>(
  setup: (props: { [K in keyof Props & RuntimeProps[number]]: Props[K] }) => void,
  options?: {
    props?: RuntimeProps;
  },
): (props: Props) => any;

Playground

@Andarist
Copy link
Contributor Author

Andarist commented Jan 5, 2024

Yeah, I understand that - in general - this other PR would change the behavior for Vue today. It's great to know how it would affect them negatively though, that I didn't know, and thanks for that!

Though, their whole signature is somewhat dubious and I don't find it to be a compelling use case that would prove that this other PR is worse than what we have today.

Their docs mention:

The main use case for this signature is with TypeScript (and in particular with TSX), as it supports generics:

But if we actually, try to utilize this then I'm not quite sure about what kind of generics support we are talking about:

import { defineComponent, ref } from "vue";
import { h } from "@vue/runtime-dom";

const Comp = defineComponent(
  <T extends string | number>(props: { msg: T; list: T[] }) => {
    const count = ref(0);

    return () => {
      return h("div", count.value);
    };
  },
  {
    props: ["msg", "list"],
  },
);

Comp;
// ^? const Comp: (props: { msg: any; list: any; } & {}) => any

Maybe I don't understand how their compiler uses this but TS-wise this leaves me quite confused since this "generics support" leaves the output totally unsafe.

I also think that their API choice for this leaves some nasty runtime holes here and that they could do better. If they need to declare required keys~ then they really should use an object for that and not an array. An array typed as Array<keyof Foo> might contain some of Foo's keys or even none of them. So while this today sort of works at type level - it isn't overly correct anyway. That's exactly why, with my proposed inference change, the array with a subset of keys is accepted.

If they would use an object to declare those props then it would work with my change and it would improve the runtime safety. The location of the error would change but I don't think that's a big deal (TS playground):

declare function defineComponent<Props extends Record<string, any>>(
  setup: (props: Props) => void,
  options?: {
    props?: Record<keyof Props, unknown>
  },
): (props: Props) => any;

const result = defineComponent(
  <T extends string>(_props: { msg: T, list: T[] }) => {
    return () => {};
  },
  {
    props: { // error reported here when not all props are declared here
      msg: true,
      // list: true // uncomment to see the inferred generic signature
    },
  },
);

@Andarist Thanks to #55811 , if #56939 lands, they could use a definition similar to the following one to error out when someone destructures a property not declared in the props array:

Nice ❤️

@jfet97
Copy link
Contributor

jfet97 commented Jan 5, 2024

If they need to declare required keys then they really should use an object for that and not an array

@Andarist historically, declaring just an array with props' names has always been the most concise way to declare props in Vue components, therefore I don't think they would be willing to drop it.

The object syntax is supported as well, of course, as you can see here.

 

I'm not quite sure about what kind of generics support we are talking about

Me neither, paradoxically they would have better support thanks to #56939 because the returned function would be generic and bounded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking Change Would introduce errors in existing code For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Status: Needs merge
Development

Successfully merging this pull request may close these issues.

6 participants