How to enforce stricter typing on Maps and Sets in TypeScript?

Viewed 171

If I create a map like so: (playground)

const x = new Map();
const y = x.get("foo");
y.thisDoesntExist();

I'd expect to get an error on the last line (at least in strict mode), because I've never specified what the key and value types of new Map should be. Instead the type of the map is Map<any, any> and so I don't get any warnings. The same happens with Set<any>.

I'm in the process of porting a JavaScript project so cases like these are all over the place and it's hard to tell where types are missing.

Is there a way to configure typescript so that it warns me when I try to instantiate Sets and Maps without generics? Or at least make the generics default to Set<unknown>?

2 Answers

Parameterless Map constructor has Map<any, any> return type:

interface MapConstructor {
    new(): Map<any, any>;
    // ...
}

Hence the noImplicitAny won't help here (the type is explicit )

What can be done is overriding it (declaration merging) with Map<unknown, unknown>:

interface MapConstructor {
    new(): Map<unknown, unknown>;
}

Now y.thisDoesntExist(); will be reported as error.

Playground


The Set behaves differently, because its constructor is defined as:

new <T = any>(values?: readonly T[] | null): Set<T>;

So if the generic type and values are not provided - the type will be unknown:

const s = new Set(); // s: Set<unknown>

update (June 28, 2022)

It seems like simply using never instead of unknown is enough to make types strict when using JavaScript + JSDoc. Not sure why I had to jump through so many hoops earlier, but now I'm just using.

interface SetConstructor {
    new(): Set<never>;
}

interface MapConstructor {
    new(): Map<never, never>;
}

Old answer

To make this work with ts in js using JSDoc, you can add the following in a new .d.ts

interface MapConstructor extends Omit<MapConstructor, 'new'> {
    new<K = unknown, V = unknown>(entries?: readonly (readonly [K, V])[] | null): Map<K, V>;
}

interface SetConstructor {
    new <T = unknown>(values?: readonly T[] | null): Set<T>;
}

In JavaScript, generics default to any when not set (#14907), so you have to explicitly set their defaults to unknown. Therefore an empty constructor doesn't work when using JavaScript.

MapConstructor has an empty new(): Map<any, any> entry in lib.es2015.collection.d.ts. So in order to remove this we use Omit<MapConstructor, 'new'>. Then we can specify the same constructor that already exists, except with explicit unknown as defaults.

SetConstructor doesn't have an empty new() entry, but it does have new <T = any>, so we'll have to override this with unknown as well.

Related