Typescript can prevent you from using indexes on the wrong array
Here's an idea that I've been toying around with. It's possible in TypeScript to create an index to a data structure that is only valid for that data structure. This method requires no runtime checks: it's verified completely at compile time!
I thought some people here may be interested in the pattern, and there are probably ways to refine this to be more ergonomic that I haven't thought of. This pattern takes inspiration from Rust's lifetimes and Haskell's ST
// An Array has a type and an Id
type IndexedArray<T, Id> = {
data: T[],
id: Id
};
// An index has an id, the id of the index must match that of the array
type Index<Id> = {
index: number,
id: Id,
}
// I can create an index with the id of the array
const push = <T, Id>(arr: IndexedArray<T, Id>, value: T): Index<Id> => {
const { data, id } = arr;
return {
index: data.push(value) - 1,
id
};
};
// I can use an ondex on an array if it has the same id
const getIndex = <T, Id>(arr: IndexedArray<T, Id>, index: Index<Id>): T => {
return arr.data[index.index];
}
// The trick here is a generic parameter inside the function
const withArray = <T, R>(callback: <Id>(array: IndexedArray<T, Id>) => R ): R => {
return callback({
data: [],
// id is actually always null, but the type signatures prevent
// this from being unified
id: null
});
}
withArray(<Id1>(a1: IndexedArray<number, Id1>) =>
withArray(<Id2>(a2: IndexedArray<number, Id2>) => {
// within this block, indexes can only be used on the array they
// were created with
const index1 = push(a1, 5);
const index2 = push(a2, 3);
getIndex(a1, index2); // Error, Id2 not assignable to Id1
getIndex(a2, index1); // Error, Id1 not assignable to Id2
const val1 = getIndex(a1, index1);
const val2 = getIndex(a2, index2);
})
);
EDIT: Made the code more obvious, a1 and a2 can both contain the same type
Posted Jun 5, 2021, 11:26 PM UTC · edited
This just seems like an object with more steps.
What do you mean?
Well basically you have a variable and you want to get a value out of it, and the way this is designed you have to use a special method to get the value out. You also need to use a special method to set the value.
Just simplify it and use an object with properties. If you don't know how many of the values you will have then use an array and then you don't access it by an arbitrary index like you do in your example.
I think you may be missing the point. The idea of TypeScript is to prevent mistakes. Say you have two arrays of unrelated things:
It doesn't matter what they are, as long as they are unrelated. Say you use a function that returns an index into the array, like
indexOfThere is nothing stopping you from using the
hatIndexon the array of plants, even though that is completely wrong:The idea behind the original post is to try to prevent that kind of mistake from happening. The original code prevents you from using an index from one array on another array by mistake, even if they have the same type. I used the "special methods" to set and get a value because it was shorter to write than a full class.
It is nice that TypeScript allows to achieve this level of safety. Though it feels to me like complicating a simple thing. It doesn’t look like the benefits are worth the complexity.
The goal of typescript is not to give developers tools to combat anti-patterns like you're suggesting, it's to help them scale well-built patterns. Your code is over-engineered and no company would enjoy seeing it in a pull request
Objects do not provide a type variable for the concrete type, so there is no type variable to use to match each index with the array/object it is an index for.
The workaround is to use a continuation to turn the existential type (the object) into a parametric type (generic) of the continuation, which is what has been done here.