Hacker News new | ask | show | jobs
by throwanem 1709 days ago
The third example describes something useful in record types, but goes about it in what seems an odd way, and ends up suboptimal as a result. I'd instead use an object type like this:

    type Human = {
      name: string;
      age: number;
    }
which also enforces value types in the compiler, rather than requiring runtime guards.
5 comments

The type itself is fine but how it is checked and used is not. The following would be better, and in this case you will also have a Human type inside the forEach callback:

    // ... other code

    type Human = { name: string; age: number }

    const isHuman = (obj: unknown): obj is Human => obj && typeof obj === 'object' && 'name' in obj && 'age' in obj; // you can complete the gaps here and also check the property types

    someArray.filter(isHuman).forEach((h) => {
      // h has type Human now
      console.log(h.age);
    })
This is reasonable for validating an untrusted object, sure, but I don't recall that constraint being expressed as part of what the original post was addressing.
The type guard may be useful when transferring that array server->client or vice versa, or for an array from any other untrusted source, but I don’t think that was the purpose of the demo in the article. When the array in question is coming from the same code base that kind of runtime type check is redundant.
I think the example is a bit contrived with a preset union. Where it’s really valuable is when you’re extracting a union from another source (like via keyof) and want to keep the two objects in sync without having to modify the keys in two places.
Which is exactly what was used in the code exemplifying something else in the second example - so that confused me (never having written any TS) too.
Yeah, typically you want a record type for something like a mapping over potentially arbitrary keys (via an index type) to values of known type, and an object type (which can have optional keys) when you do know exactly what shape you expect and want to enforce it.
I found that pattern useful when you don't know what the key will be. I have an iOS app that tracks tips, when you add a tip, it's stored in an object like this:

type Tips = { [tipGuid: string]: TipObject }

which can be rewritten using Record as

type Tips = Record<string, TipObject>

That pattern ins't very useful when creating object with known keys but for data structures where the key is either not known or generated it a godsend.

`Record` is a dangerous type, and I would recommend against it. The problem is that it assumes any key is valid and will return a value type. Example:

    type Tips = Record<string, TipObject>
    const tips: Tips = {}
    tips["hello"]  // TipObject, but you actually get undefined

It's better to define a Dictionary type like so:

    type Dictionary<K extends string | number | Symbol, V> = Partial<Record<K, V>>
Then to use:

    type Tips = Dictionary<string, TipObject>
    const tips: Tips = {}
    tips["hello"]  // TipObject | undefined
That way, you're always forced to check for existence, and you never accidentally attempt to access properties on `undefined`.
It’s not true that Record will result in a type where any key is valid. If you pass in a primitive like string, then of course any string will be valid. That’s not Record’s fault; what you’re doing is essentially creating an index signature [1]. If you pass a more restrictive type in as the key, it works as expected:

    type Tips = Record<“foo”, TipObject>;
    const tips: Tips = {}; // error, needs key “foo”
    tips["foo"]; // fine
    tips["bar"]; // error, no key “bar” in tips
It’s worth mentioning that this isn’t just an issue with objects. For example, by default, the index type on arrays is unsafe:

    const arr: number[] = [];
    const first: number = arr[0]; // actually undefined, but typescript allows it
If you do need an index type and want to account for undefined keys, the idiomatic way is the noUncheckedIndexAccess compiler flag [2], which will automatically make any index property access a union with undefined.

[1] https://www.typescriptlang.org/docs/handbook/2/objects.html#...

[2] https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAc...

You’re right that this is really easy to mess up, especially when defining an array index e.g.

    const todos: string[] = [“walk dog”];
    todos[123].toUpperCase() // error!
IMO non constant (as defined by TypeScript) arrays should’ve been automatically assigned a union type with `undefined`, which can also be a fix for Records too:

    type Tips = Record<string, TipObject | undefined>
You’re looking for the noUncheckedIndexAccess compiler option: https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAc...
While this is probably alright for some data, I'd definitely recommend using something like a Map instead (especially if the object mutates) for things you have control over (ie it's not describing an endpoint or something similar).
I think the example type is useful for unvalidated input data e.g. from an API call. Or an update function for a DB abstraction.

Internally in the backend, you’d still use the type you just posited.

Eh. A good ORM provides type definitions from model definitions, which is one way I've found ORMs more useful in TS than JS, and I'd more likely use a runtype or a decoder to both validate and type inbound data than roll my own interface for it.

On review of documentation, I was actually pretty off base in grandparent comment. The real use case for Record appears to be when you need a map type whose keys are both explicitly enumerated and defined elsewhere, ie in a union, enum, or otherwise unrelated object type. Rather than duplicating the keys, you can use Record<someUnion, V> or Record<keyof typeof someEnum, V> and only have to make one change to update both.

For the "arbitrary keys, known value types" case I mentioned earlier, an object type with an index signature works fine and may be more legible.

> an object type with an index signature works fine and may be more legible.

If I understand you correctly, that's exactly what a Record is underneath

More or less. There are extra steps involved with a Record, but they work the same iirc.