How to use Index Signatures for Dynamic Data
Notes from Effective TypeScript
Search for a command to run...
Notes from Effective TypeScript
No comments yet. Be the first to comment.
This series shares lessons learned from Effective TypeScript as I read through the book.
Notes from Effective Typescript
Short answer is: You can’t But there are other options! These options help you: Expand the … N more … values in your intellisense pop-up window Expand Type contracts to view all properties in a combined type without using the subtype names. 1 - U...
Introduction eBPF (extended Berkeley Packet Filter) is a successor to the BPF (Berkeley Packet Filter) which already existed as part of the Linux kernel as far back as 1992. It allows developers to safely extend the kernel based on sys-call events, a...
I'm about to host my own ATProto server. It's 01:09am my time, but I want to do this now, before bed, so that it doesn't distract me from important work tomorrow. If you don't know what this is visit: I have my first issue. The issue is that my doma...
I'm about to do my first ever domain transfer. It's from Squarespace to Cloudflare, and I hope it goes smoothly. I need to do this because Squarespace is woefully under-featured for the needs of building an indie sass product. I have a major concer...
And why you should be very intentionally optimizing your workflows with AI tools - they really help!
We mean data which changes or can change. An example given is data in a spreadsheet where we don't even know the column names, nor do we know what values will exist in the rows. Think weather data, students who will enroll in a school next year, and server environment variables -- these are all examples of dynamic data.
Index signatures are a special way Typescript lets us specify the type of an object whose keys and values are dynamic.
Bring to mind a row in a spreadsheet representing different types of rockets. Challenge yourself to create a type for an object representing a row in this spreadsheet without any idea what the column names are, or how many columns there are.
Here's an index signature solving this problem:
type RocketRow = {
[key: string]: string // <-- this is the index signature
};
This is the structure of an index signature:
[key-name: key-name-type]: key-type
Lack of precision.
We cannot specify a precise:
Nor can we have unique types per key.
Basically anything that matches the key type or the value type goes, and you can have any number of keys, including an empty object {}.
Due to this lack of precision we lose TypeScript's language services like autocomplete and inference when working with index signatures.
For truly dynamic data. We'll use an example from Effective TypeScript to illustrate this.
The following example of a CSV parser uses an index signature to represent the row objects it returns.
This is great! Because we're freed of the need to worry about how many columns exist in the spreadsheet, or to know ahead of time exactly what the column names are when forming our objects.
function parseCSV(input: string): {[columnName: string]: string}[] {
const lines = input.split('\n');
const [headerLine, ...rows] = lines;
const headers = headerLine.split(',');
return rows.map(rowStr => {
const row: {[columnName: string]: string} = {};
rowStr.split(',').forEach((cell, i) => {
row[headers[i]] = cell;
});
return row;
});
}
Vanderkam, Dan. Effective TypeScript . O'Reilly Media. Kindle Edition.
Following from our previous example, and borrowing again from the book, say in the future we get to know more specifically what our row type is, we can assert this as the output type of parseCSV.
In this example we define an interface for our row types then assert the return value of ParseCSV as an array of ProductRow objects.
interface ProductRow {
productId: string;
name: string;
price: string;
}
declare let csvData: string;
const products = parseCSV(csvData) as unknown as ProductRow[];
For the curious:
The danger is that we can never really be 100% sure that the data we get will be what we expect.
Continuing from the ProductRow example above, someone might mistakenly omit a field in the spreadsheet or put in a value of a wrong type.
We accept that sometimes we may get values of some other type or none at all in which case we would expect undefined. So we expand the value type of our index signature to reflect this. For example:
function safeParseCSV(
input: string
): {[columnName: string]: string | undefined}[] {
return parseCSV(input);
}
This adds some protection against runtime errors if say we try to loop through the row objects returned by parseCSV
const safeRows = safeParseCSV(csvData);
for (const row of safeRows) {
prices[row.productId] = Number(row.price);
// ~~~~~~~~~~~~~ Type 'undefined' cannot be used as an index type
}
There are certain keys which exist on all JavaScript objects through Object.prototype. One of them is constructor.
To illustrate, running the following in the typescript playground shows us that quirk has a constructor key even though we never defined one.
const quirk: {[key: string]: number} = {hello: 1}
console.log(quirk['constructor'])
// console output: ƒ Object() { [native code] }
Using Map/Set types over index signatures allows us to avoid quirks such as accessing Object.prototype values when accessing the keys of an object built using an index signature.
For the curious:
Don't use an index signature, use optional types or a union type.
interface Row1 { [column: string]: number } // Too broad
interface Row2 { a: number; b?: number; c?: number; d?: number } // Better
type Row3 =
| { a: number; }
| { a: number; b: number; }
| { a: number; b: number; c: number; }
| { a: number; b: number; c: number; d: number };
Vanderkam, Dan. Effective TypeScript . O'Reilly Media. Kindle Edition.
Sometimes keys are limited to string values such as 'x', 'y', 'z'. Using a string type in this case is way too broad.
Use a record type or a mapped type
type Vec3D = Record<'x' | 'y' | 'z', number>;
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
type Vec3D = {[k in 'x' | 'y' | 'z']: number};
// Same as above
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
// Type ABC = {
// a: number;
// b: string;
// c: number;
// }
Vanderkam, Dan. Effective TypeScript . O'Reilly Media. Kindle Edition.