Typescript: Preventing generic string keyed objects from overlapping with Arrays

0 Comments

 This was a bit of a doozy and took me the better half of a day to find a semi decent fix for it.

Here's the unsuspecting use case and why it caused problems (try it out on Typescript playground)

// --- Type definitions
type SingleConfig = {
[key: string]: any;
// [index: number]: never; // Works fine if you don't use code like [variable.name] below
lastIndexOf?: never; // Correctly forces detection of arrays
}
type MultiConfig = SingleConfig[];
// --- The subtle, bug causing function
function conditionalReturn(gimme: 'single' | 'multiple'): SingleConfig | MultiConfig {
if (gimme === 'single') {
return { single: 'value' };
}
else {
return [
{ multiple: 'values' },
{ and: 'more!' },
];
}
}
// --- The unknowing usage
function onlyWorksWithSingleConfig(config: SingleConfig) {
console.log('onlyWorksWithSingleConfig is array', Array.isArray(config), config);
}
function isSingleType(what: any): what is SingleConfig {
return !Array.isArray(what);
}
function doSomething() {
const single = conditionalReturn('single');
onlyWorksWithSingleConfig(single); // This should complain
const multiple = conditionalReturn('multiple');
onlyWorksWithSingleConfig(multiple); // This should complain too
const variable = { name: 'santa', value: 'clause' };
const variableConfig: SingleConfig = {
fixed: 'value',
[variable.name]: variable.value,
};
// Use of type guards failed
if (isSingleType(single)) {
onlyWorksWithSingleConfig(single); // This is fine
}
}

  • SingleConfig and MultiConfig are different types. One is a single object value, the other is an array of that.
  • conditionalReturn() returns either one of those depending on the input.
  • onlyWorksWithSingleConfig() as the name states will only work with an instance of SingleConfig
  • Typescript doesn't seem to detect an issue when using onlyWorksWithSingleConfig() with the result from conditionalReturn()

Why? Because Javascript is a hot mess when Arrays are considered to be objects. It is possible that Typescript allows that behaviour because Javascript arrays are keyed by strings (not numbers) so it will fit the type interface specified by SingleConfig.

For instance, look at this output.


So now that we have a possible reason why this is allowed in Typescript, how can we prevent it?

Attempt 1 - use type guards

As documented here, I tried using them to enforce some sort of sanity. It did not work.

Attempt 2 - disallow [index: number] in SingleConfig

I found this on StackOverflow and thought it was an elegant solution to the problem, however quickly realised it broke a valid use case where this syntax would be marked as incorrect.


Attempt 3 - Disallow "random array prototype member" ✔

A smart fellow at work suggested being a bit more specific with the "never" clause.

Instead of trying to capture all the cases, why not filter out a specific case, like "length"? (Just to be safe, I used something a bit more weird like "lastIndexOf" to avoid potential issues)

And it worked! Just like that, Typescript is now correctly detecting the two separate types.


It even fixed up the type guard function isSingleType() !

 
Copyright © Twig's Tech Tips
Theme by BloggerThemes & TopWPThemes Sponsored by iBlogtoBlog