Skip to content

Conversation

@mattwoolford
Copy link
Contributor

  • Enable literal types on the type map object so that MIME types are accessible in Typescript.
  • Preserve type safety of {[key: string]: string} using Typescript satisfies.

- Enable literal types on the type map object so that MIME types are accessible in Typescript.
- Preserve type safety of `{[key: string]: string}` using Typescript `satisfies`
- Enable literal types on the type map object so that MIME types are accessible in Typescript.
- Preserve type safety of `{[key: string]: string}` using Typescript `satisfies`
@broofa
Copy link
Owner

broofa commented Sep 11, 2025

Hi Matt, thanks for the PR.

Can you give me an example or two of how this is useful? I'm not familiar with satisfies, so I don't really understand what problems this solves.

@mattwoolford
Copy link
Contributor Author

mattwoolford commented Sep 12, 2025

Hi Robert,

No worries, of course.

1. The problem and what this PR fixes

As this was explicitly typed as {[key: string]: string[]}, it meant that literal types could no longer be inferred by Typescript.

Take the following example:

function checkMIMEType(file: File, type: string) {
    return file.type === type;
}

I could use this function as follows:

checkMIMEType(someFile, "text/xml");

However, it would be extremely useful if I could use a literal type for the type parameter as the following would never be true:

We don't want to allow:

checkMIMEType(someFile, "some string") // ⚠️ Would always be false because the type parameter isn't a MIME type

checkMIMEType(someFile, "txt/xml") // ⚠️ Would always be false as there is a typo in the MIME type (type parameter)

Instead, we want to allow:

checkMIMEType(someFile, "text/xml"); // ✅ since "text/xml" is a valid MIME type in the database

Given this library, I wouldn't want to create my own database of possible MIME types, so the solution should be:

import standardTypes from "mime/types/standard.ts";

type MIMEType = keyof typeof standardTypes; // ✅ MIMEType must be one of the keys in the database

function checkMIMEType(file: File, type: MIMEType) {  // 🎉 `type` is not just any old string anymore
    return file.type === type;
}

What this PR solves is that, where the type was set to {[key: string]: string[]}, then keyof typeof standardTypes was always a string, not explicitly one of the available MIME types, which is what we want. So to achieve this, {[key: string]: string[]} should be removed, resulting in:

Before:
image

After:
image

So now:

checkMIMEType(someFile, "some string");
// ❌ Before: Typescript would allow, because "some string" is a string
// ✅ After: Typescript will not allow, because "some string" isn't one of the MIME types

checkMIMEType(someFile, "txt/xml");
// ❌ Before: Typescript would allow, because "txt/xml" is a string
// ✅ After: Typescript will not allow, because "txt/xml" isn't one of the MIME types

checkMIMEType(someFile, "text/xml");
// ⚠️ Before: Typescript would allow, because "text/xml" **is a string**
// ✅ After: Typescript will allow because "text/xml" **is recognised as a MIME type**

This was not possible before.


2. Maintaining the type safety of the database (satisfies)

Using Typescript's satisfies retains the type constraints as applied on the original database, in spite of the fix:

We don't want:

// If I remove the type specification on `types`, then I could put anything in as a value:
const types = {
    // ...
    "text/xml": 78459387453 // ⚠️ Valid when {[key: string]: string[]} isn't specified as the type
}

Instead we want:

// If I remove the type specification on `types`, but add a `satisfies` constraint, then I can only put something valid in as a value:
const types = {
    // ...
    "text/xml": 78459387453 // ❌ Typescript error: 78459387453 does not satisfy type "string[]"
} satisfies {[key: string]: string[]}

I then noted that the database is made readonly by Object.freeze(), but would you believe, Typescript pays no attention to this.

We don't want:

import standardTypes from "mime/types/standard.js"

standardTypes["application/json"] = ["something else"] // ⚠️ Valid in Typescript, even if it is not possible at runtime because of `Object.freeze()`

Instead we want:

// /types/standard.ts

const types = {
    // ...
} as const satisfies Readonly<{[key: string]: string[]}>

&

import standardTypes from "mime/types/standard.js"

standardTypes["application/json"] = ["something else"] // ❌ Not valid in Typescript because this is a readonly field

Hence the {[key: string]: string[]} type definition was removed, and the trailing as const satisfies Readonly<{[key: string]: string[]}> definition has been added.

Hope that helps!

Matt

@broofa
Copy link
Owner

broofa commented Sep 12, 2025

Great explanation, thank you.

So... next question: Is this a semver "patch", "minor", or "major" (breaking) change?

(I'm tempted to say it should be major because it changes the TS api types in a way that may break tsc compilation for users. But... it doesn't actually change the JS api.)

@mattwoolford
Copy link
Contributor Author

mattwoolford commented Sep 12, 2025

I would say this is a minor update per the SemVer spec. 😁

My reasoning:

  • The type of the databases haven’t changed, it’s just enforced in a different way
  • Therefore these types are backwards compatible, since the databases must still ”satisfy” {[key: string]: string[]}
  • New functionality is enabled
  • A bug (related to Object.freeze()) is patched

@broofa broofa merged commit 7de528d into broofa:main Sep 12, 2025
@broofa
Copy link
Owner

broofa commented Sep 12, 2025

Published as [email protected]

Thanks for the contribution. I particularly appreciate the time you took to explain everything. 👍

@mattwoolford
Copy link
Contributor Author

You’re welcome. Happy to help and great to see it merged! 😁🎉

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

Successfully merging this pull request may close these issues.

2 participants