jsr icon indicating copy to clipboard operation
jsr copied to clipboard

Slow types: Better detection/fallback for complex constant declarations

Open bradenmacdonald opened this issue 1 year ago • 3 comments

I often use a pattern in my code where some object has a lot of keys, and I want TypeScript to automatically detect the keys and make them part of the type.

For example, in quantity-math-js:

/** SI prefixes */
export const prefixes = Object.freeze(
    {
        ...
        k: 1e+3,  // kilo
        M: 1e+6,  // Mega
        G: 1e+9,  // Giga
        ..
    } as const satisfies Record<string, number>,
);

I want for example to use that to define a related type using all the keys: Screenshot 2024-03-03 at 2 13 43 PM

Here are two other examples of this pattern with even longer type definitions (more keys):

A second example: units.ts A third example: Icon.tsx
Screenshot 2024-03-03 at 2 21 41 PM Screenshot 2024-03-03 at 2 19 14 PM

Now, not surprisingly, deno lint and deno publish complain that these are "slow types". But given how complex the types are (see examples), I don't want to duplicate them in the code. And given how useful it is to have the keys available in my IDE with auto-completion, I don't want to simplify the types either.


I think a nice compromise solution would be to degrade these types to whatever type is specified using the satisfies operator, when necessary. So if I slightly re-write my declaration as follows:

export const prefixes = Object.freeze(
    {
        ...
        k: 1e+3,  // kilo
        M: 1e+6,  // Mega
        G: 1e+9,  // Giga
        ..
    } as const,
) satisfies Record<string, number>;

Then ideally, deno and typescript and other first-class users of the code will have the full, detailed type with all the exact keys available, but for JSR's purposes of documenting this and transpiling it for Node.js or whatever other things need to avoid slow types, it should just simplify the type of this to Record<string, number> which is OK to do because of the explicitsatisfies declaration

Would that be possible?

bradenmacdonald avatar Mar 03 '24 22:03 bradenmacdonald

satisfies can't be used, because the resolved type of that is:

const prefixes: Readonly<{
    readonly k: 1000;
    readonly M: 1000000;
    readonly G: 1000000000;
}>

That said, I think it could be improved to understand this scenario.

dsherret avatar Mar 04 '24 13:03 dsherret

OK, well if it can just understand the full type, that's even better :) I just want to avoid having to duplicate the type and maintain the list of valid keys in two or more different places.

bradenmacdonald avatar Mar 04 '24 16:03 bradenmacdonald

The same problem occurs when using zod schemas. They are entirely built on inference and a static defintion completely undermines their purpose.

My current workaround:

  1. Change the zod import to import {z} from "zod"
  2. $ yarn add zod
  3. $ tsc --emitDeclarationOnly --declaration --target esnext --moduleresolution nodenext --module nodenext schema.ts
  4. Copy-paste the declaration to schema.ts and use it as static type
  5. Delete all generated files and change back the import

Without knowing anything about the internal structure of jsr, I would suggest infering all slow types and automatically inserting them at publish time might solve the problem. This would also help to avoid class members like this: readonly foo: Map<string,number> = new Map()

hansSchall avatar Mar 28 '24 16:03 hansSchall