1

I want to define the object type that I download from my database.

type ActiveOrders = {[orderId: string]: {name: string; price: number}}

const activeOrders: ActiveOrders = {
  'orderId1': {name: 'apple', price: 123},
  'orderId2': {name: 'banana', price: 123},
  'orderId3': {name: 'tesla', price: 99999999},
}

Following code is fine, and my orderData is guaranteed to exist.

for(const orderId in activeOrders) {
  const orderData = activeOrders[orderId]
  // This is fine, orderData is guaranteed to exist
  const {name, price} = orderData
}

This is NOT fine, but typescript is not giving me any error. someRandomId can come from anywhere such as user entered value.

const orderData2 = activeOrders['someRandomId']
// This is NOT fine, orderData2 is possibly undefined, but typescript says it is guaranteed to exist.
const {name, price} = orderData2

I can change my type to following but I want to avoid as it will mess up my for-in loop.

type ActiveOrders = {[orderId: string]: {name: string; price: number} | undefined}

Is there more elegant solution to this?

Eric Kim
  • 10,617
  • 4
  • 29
  • 31
  • do you want to make sure that the keys always start with `orderId`? – Ramesh Reddy Jan 25 '22 at 06:18
  • 2
    You need to use the `noUncheckedIndexedAccess` option in your tsconfig but that would make both cases throw errors. As far as I know there's no way to get the first to work and the second to fail – apokryfos Jan 25 '22 at 06:23
  • It isn't Typescript's job to validate your orderIds - you've told Typescript to expect a string [orderId] key in an object, and that's the only thing it will know/care about. It's your job to then validate the orderId; if you're concerned that it's possible that invalid orderIds will be used to access the object, you should type it accordingly (as you've done using `undefined`). – Brendan Bond Jan 25 '22 at 06:25
  • @RameshReddy no, my orderIds are randomly generated – Eric Kim Jan 25 '22 at 06:28
  • @apokryfos Thank you for the info! Just wondering, instead of using string as key, does typescript allow me to create symbol? that way I can say if I use string as key it will be possibly undefined. However, symbol can also be used a string – Eric Kim Jan 25 '22 at 06:35
  • symbols are valid for indexing objects but I'm not sure how your type would look like. E.g. something like: `{[orderId: string]: {name: string; price: number}|undefined}|{[orderId: symbol]: {name: string; price: number}}` ? – apokryfos Jan 25 '22 at 06:48
  • @apokryfos Like this but my for-in loop is saying orderId is always string for some reason. `type ActiveOrders = { [orderId: symbol]: {name: string; price: number}; [orderId: string]: {name: string; price: number} | undefined; }` – Eric Kim Jan 25 '22 at 06:51
  • 1
    yes typescript will try to determine which type something is when there's a type union involved. Check my answer for a way to do this with a conditional type – apokryfos Jan 25 '22 at 07:00

2 Answers2

2

By definition TypeScript will assume that any key you use to access this type of object is valid because you basically said it would be.

You can use noUncheckedIndexedAccess to force typescript to assume that there is always a chance the result is undefined when using any arbitrary string to index the object.

You can use a conditional type to specify when the object is assumed to always be defined and when it could be undefined (or anything really):

type ActiveOrders = { 
  [orderId in string|symbol]: orderId extends string ? ({
    name: string; 
    price: number
    }|undefined) : {
    name: string; 
    price: number
    }
}

const keys : symbol[] = [
  Symbol('a'),
  Symbol('b'),
  Symbol('c')
];

const activeOrders: ActiveOrders = {
  [keys[0]]: {
    name: 'apple', 
    price: 123
  },
  [keys[0]]: {
    name: 'banana', 
    price: 123
  },
  [keys[0]]: {
    name: 'tesla', 
    price: 99999999
    },
}
for(const symbolKey of keys) {
  const orderData = activeOrders[symbolKey]
  // This is fine, indexed with a symbol the data is assumed to be there
  const {name, price} = orderData
}

Playground link

apokryfos
  • 38,771
  • 9
  • 70
  • 114
  • Thank you for the answer. I don't think I can use your symbol as a solution since `for-in` or `Object.keys` forces my keys to be string (which is fine probably JS restriction). I will still take this as a solution since you gave possible workaround `noUncheckedIndexedAccess`. – Eric Kim Jan 25 '22 at 07:19
  • [Object.getOwnPropertySymbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols) might work for iterating over symbol keys – apokryfos Jan 25 '22 at 08:40
1

The ActiveOrders type you've defined reads as follows:

// An ActiveOrders is a map...
type ActiveOrders = {
    // with arbitrary string keys in it that correspond to map values...
    [orderId: string]: {
        // that must contain a `name` key whose value is a string
        name: string;
        // and a `price` key whose value is a number
        price: number;
    }
};

By this definition, activeOrders['someRandomId'] is valid. The ActiveOrders type allows for any string key to exist within its map, and the type does not know what those keys will be.

With TypeScript 4.1+, you do have some freedom over defining what constitutes an acceptable key, but remember that, while you may be coding with TypeScript, your code is being transpiled to JavaScript where your type is no longer upheld.

So it really comes down to you to safety check each datum; the types are just there as guard rails for developers. If you want some sort of runtime verification of the data's shape, you could use a schema validator like yup, but this seems weirdly placed next to TypeScript.

Zulfe
  • 820
  • 7
  • 25