Skip to content

Instantly share code, notes, and snippets.

@chriseppstein
Last active November 19, 2017 21:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriseppstein/4b582388732608c60cac1890e3a9e4d6 to your computer and use it in GitHub Desktop.
Save chriseppstein/4b582388732608c60cac1890e3a9e4d6 to your computer and use it in GitHub Desktop.

ItemType Generic Utility Type

This is to describe the situation where one might want to use the type of an array indirectly.

The ItemType generic is sugar over an existing capability in TypeScript to access the type information of the Array generic and make accessing that type more legible to the reader. But a few people replied with "huh, I don't think you want to do that", and to their point, I agree... mostly.

It's almost always better to declare a variable to be of the Array's type directly.

But I'm finding there are situations where I linking a local variable's type to an interfaces's member type to be more maintainable because it helps the compiler direct me to type errors as I refactor code more effectively.

So here's a concrete use case that is simplified situation where I would use this pattern and I'll explain why afterwards.

interface Association<Key, Value> {
key: Key;
info: Value;
}
interface StringKeyInfo {
keyIndexes: Array<Association<string, number>>;
keyIsSet: Array<Association<string, boolean>>;
}
class StringKeys {
private info: StringKeyInfo;
keys: Array<string>;
constructor() {
this.info = initializeStringKeyInfo("");
this.keys = [""];
}
add(key: string) {
let index = this.keys.length;
this.info.keyIndexes.push({key, info: index});
this.info.keyIsSet.push({key, info: true});
}
}
function initializeStringKeyInfo(key: string): StringKeyInfo {
let keyIndexesDefault: ItemType<StringKeyInfo["keyIndexes"]> = {key, info: 0};
let keyIsSetDefault: ItemType<StringKeyInfo["keyIsSet"]> = {key, info: true};
return {
keyIndexes: [keyIndexesDefault],
keyIsSet: [keyIsSetDefault]
};
}

The way I used to write the constructor above would be much simpler:

this.info = {
  keyIndexes: {key: "", info: 0},
  keyIsSet: {key: "", info: true}
};
this.keys = [""];              

This is nice and terse. There's no messy type information mucking up your code. you can get right to using your object, and typescript totally sorts this all out when you finally get to the place where you assign this initial value to a place where the type is declared. This is really great and most of my typescript code looks like this. I would even write this particular code like this even today, it's just not complex enough to warrant the actual situation where this becomes useful, but for an example, I've tried to reduce the example down to something indicative but no so complex as to obfuscate the main point.

But what I find is that there are complex interfaces that do not warrant becoming a class and the overhead it would entail. In these sitatuations there are usually some module-scoped functions that act as if they were class-methods but are unbound in the module scope. Since they are natually linked to the interface, I want to tightly bind local variables to the interface itself -- not unlike what a class gives me with types for class members, in this way there are fewer changes to make to the code as it evolves over time.

The other reason this is beneficial is that typescript obviously doesn't do any type checking until you get to the place in code where types are declared. When that place is a complex interface the error messages get really unweidly and decoupled from where the type error was introduced so I find myself declaring intermediate types along the way so that TypeScript can give me errors much earlier and closer to the code that's actually broken.

At the intersection of these two situations, I find it useful to declare a local variable with a type of an array instead of declaring it explicitly -- and doubly so when the type of the array is itself a generic -- because in that sitation, if the generic changes, we're back to not detecting the conflict until we get to the complex type assignment.

In the case of a generic array type, we can work around this by declaring a type alias to the generic. E.g. type StringToNumberAssociation = Association<string, number>;. And, yes, that's true. Do that where it makes sense. On a case-by-case basis, though, sometimes I think it's cleaner to work backwards from an interface's member's type declaration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment