Skip to content

const Type Parameters fail when mapped #54537

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

Closed
Peeja opened this issue Jun 5, 2023 · 3 comments
Closed

const Type Parameters fail when mapped #54537

Peeja opened this issue Jun 5, 2023 · 3 comments

Comments

@Peeja
Copy link
Contributor

Peeja commented Jun 5, 2023

Bug Report

🔎 Search Terms

const Type Parameters, mapped types, self types

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about const Type Parameters (but there's nothing there about them yet)

⏯ Playground Link

Playground link with relevant code

💻 Code

type WithSchema<Self> = {
  // This ensures Self is inferred to be the entire object this type is applied
  // to, while the resulting type leaves the *values* of those keys as
  // `unknown`.
  [K in keyof Self]?: K extends never ? Self[K] : unknown;
} & (("schema" extends keyof Self ? Self["schema"] : {}) extends infer Schema
  ? {
      [K in keyof Schema]?: Schema[K] extends "string"
        ? string
        : Schema[K] extends "number"
        ? number
        : never;
    }
  : never);

declare function doSomething<const Self>(o: WithSchema<Self>): void;

// doSomething's Self isn't automatically `as const`, despite declaring `const Self`.
doSomething({
  schema: {
    name: "string",
    age: "number",
  },
  name: "Alice",
  age: 35,
});

// Explicitly using `as const` works.
doSomething({
  schema: {
    name: "string",
    age: "number",
  },
  name: "Alice",
  age: 35,
} as const);

🙁 Actual behavior

doSomething's Self parameter was not inferred as const.

🙂 Expected behavior

doSomething's Self parameter should be inferred as const, since it's marked as const and inferred from a literal at the call site.

More context

I'm actually working on something more complex than this: typing JSON-LD documents. The core of the issue, though, is that the type of some keys of the object depends on another key (here, schema). I'm doing this using a type parameter which infers as the type of the given object. This uses the same technique described in #52088 (which proposes a more native version using a new keyword).

Currently, the only way to make these types useful is to ask the user of these types to use as const everywhere. Of course, this is less than ideal, which is exactly why const Type Parameters were introduced. But it appears the more complex usage I have breaks that feature, reverting to inferring a widened type.

@RyanCavanaugh
Copy link
Member

Looking at the definition, this seems like arguably the intended behavior. WithSchema intentionally doesn't present any top-level definition of self, so there isn't any hint that says that a usage of WithSchema<Self> should get the constness of the type parameter.

Without reading the commentary here, I might think that someone did all this work in WithSchema just to avoid having the constness propagated into the argument expression.

@whzx5byb
Copy link

whzx5byb commented Jun 6, 2023

@Peeja

I have a workaround for you.

- declare function doSomething<const Self>(o: WithSchema<Self>): void;
+ declare function doSomething<const Self>(o: [Self] extends [unknown] ? WithSchema<Self> : Self): void;

I'm not quite sure but it seems that if you have a naked Self in either branch of a conditional type ([Self] extends [unknown] ? WithSchema<Self> : Self), the compiler will treat Self as const even it is never used.

@Peeja
Copy link
Contributor Author

Peeja commented Jun 6, 2023

@RyanCavanaugh Yeah, that does make sense.

@whzx5byb Ooh, that's perfect! I've reworked it a bit to make it more readable and more reusable. Here's InferringSelf!

/**
 * Hints TS to infer `Self` as the object being typed, but actually resolves to
 * `T`. (Generally, `T` should be defined in terms of `Self` to be useful.)
 */
type InferringSelf<Self, T> = Self extends unknown ? T : Self;

declare function doSomething<const Self>(
  o: InferringSelf<Self, WithSchema<Self>>
): void;

This also takes over all of the Self magic, so the mapped type at the beginning of WithSchema can be the much more reasonable and meaningful { [K in keyof Self]?: unknown }.

I'm happy considering this a real solution and not just a workaround. @RyanCavanaugh, I didn't see anything in your comment that sounded like you saw what you'd consider a bug here, so I'll close. If you did see something worth addressing, obviously feel free to re-open.

Thanks, both! ❤️🎉

@Peeja Peeja closed this as completed Jun 6, 2023
Peeja added a commit to m-ld/m-ld-react-demo that referenced this issue Jun 16, 2023
conorbrandon added a commit to conorbrandon/ts-dynamodb that referenced this issue Jan 20, 2024
…ombinations; increate max items to 100, was erroring because I was using an old version of ddb local (https://aws.amazon.com/about-aws/whats-new/2022/09/amazon-dynamodb-supports-100-actions-per-transaction/); note that all of this only works because of the workaround described in this issue microsoft/TypeScript#54537
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants