Just a typescript question here... I am using a li...
# random
g
Just a typescript question here... I am using a library called "runtypes" to define all of my item types and to preform runtime validation. They have a feature called "pick" where I can reduce a type down to certain fields. Whenever I get an item from dynamo I want to support sending attribute projection expression to get only the fields I need and I am trying to figure out how to get the return type on the function correct. Here's what I currently have:
Copy code
import { Record, String } from 'runtypes'

const DynamoItem = Record({
  id: String,
  something: String,
  else: String
})

export const getItem = async (id: string, attributes?: Parameters<typeof DynamoItem.pick>) => {
  const { Item } = await dynamo.get({ id }, { attributes })

  if (!Item) throw new Error('No item')

  return attributes ? DynamoItem.pick(attributes).check(Item) : DynamoItem.check(Item)
}
Currently its always the full "DynamoItem" type even when I call
getItem('myItemId', 'id')
a
Copy code
const DynamoItem = Record({
  id: String,
  something: String.optional(),
  else: String.optional()
})
This should help.
Anything that you won’t always have should be optional.
g
That doesn't really solve it just makes me have to manually check if I do actually have a value and that's not what I want.
The current code I have works perfectly if I am not specifying which parameters I want dynamo to return. Calling
getItem('myItemId')
shows the type to be the full "DynamoItem" which is what I want. Was hoping to be able to pass in an array of attributes I want to retrieve from dynamo and my return type reflect that I only have a subset of the attributes. Calling
getItem('myItemId', ['id'])
should show as type
{ id: string }
getItem('myItemId', ['id', 'something'])
should show as type
{ id: string; something: string }
etc
a
In that case you’ll have to check the keys and values separately probably.
t
You needto use a generic here
try this
let me write it out one sec
I'm not entirely familiar with the runtypes library but if they're doing things right and it's similar to
zod
this should work
Copy code
function getItem<T extends Parameters<typeof DynamoItem.pick>>(id: string, attributes: T) {
  const { Item } = await dynamo.get({ id }, { attributes })

  if (!Item) throw new Error('No item')
  return attributes ? DynamoItem.pick(attributes).check(Item) : DynamoItem.check(Item)
}
g
Hmm that doesnt seem to work either guess I will have to play around a bit see if I can get anything that works
t
What do you get as a return type there?
If it doesn't work you can cast to
as T
when returning and that should work
g
I still get the full item, I will try that
t
The full item as the type?
g
Yes
t
Took a look at the library, I can put together an example of how to do this in a bit
g
Oh thank you 🙂
t
This isn't exactly right because it uses an any but achieves what you want
Copy code
const Thing = Record({
  a: String,
  b: String,
  c: String,
});

type ThingPickable = Parameters<typeof Thing.pick>;

function narrow<T extends ThingPickable>(...attributes: T) {
  const item: unknown = {};
  return Thing.pick<T[number]>(...(attributes as any)).check(item);
}

const narrowed = narrow("a", "b");
need to fiddle a bit more
Another option without any but is also not 100% right because it uses `as`:
Copy code
type ThingPickable = Parameters<typeof Thing.pick>;

function narrow<T extends ThingPickable>(...attributes: T) {
  const item: unknown = {};
  const result = Thing.pick(...(attributes as any)).check(
    item
  );

  return result as Pick<Static<typeof Thing>, T[number]>
}

const narrowed = narrow("a", "b");
This one is stumping me lol - I thought I was a typescript master!
g
Haha yeah this one is wild, thanks for that though I dont really mind using the as any and it does work as expected now. Appreciate the help
ö
Interesting library by the way 👀 What about class-validator, it is easier to use
JS / TS world is hard. There is too much library
it just feels deppressing to find the best one
g
Classes wouldnt really be possible for parsing incoming data at least not as easy and even then - I really dislike classes unless it's specifically something like SST/CDK resources
ö
you can use class-transformer
They are usually used together. They have a method for converting both ways: plainToClass and classToPlain. class-transformer and class-validator play hand in hand. I would recommend to take a look at them
they are fairly simple, u dont really write the class, you use the class because it creates a new type
g
I prefer to stay away from using classes in my code functional is just easier and runtypes is really nice and trying to solve the problem like the one in this post would probably be even more annoying using class based components 😅
ö
okay
g
@thdxr If you ever have time I am back at this again because I have copy pasted the same function over and over for creating a filtered get result from the db and was hoping to not have to do that if possible... I tried this but the return type is { [key: string]: unknown }
Copy code
import * as Rt from 'runtypes'

const UserItem = Rt.Record({
  name: Rt.String,
  email: Rt.String,
  id: Rt.String
})

type Model = {
  get: (id: unknown, opts: unknown) => Promise<unknown>
}

const UserModel: Model = {
  get: async (id: unknown) => ({ id, name: 'test', email: '<mailto:test@test.com|test@test.com>' })
}

const createFilteredGet = <T extends Rt.Record<{ [key: string]: Rt.Runtype }, false>>(runtype: T, model: Model) => {
  return async <A extends Parameters<typeof runtype.pick>>(id: unknown, attributes?: A) => {
    const item = await model.get(id, { attributes })
    
    if (!item) return null
    
    return attributes ? runtype.pick<A[number]>(...attributes as any).check(item) : runtype.check(item)
  }
}

const getUser = createFilteredGet(UserItem, UserModel)

getUser('1', ['id']).then(item => {
  // item should be typed as { id: string } | null but currently { [key: string]: unknown } | null
  console.log(item)
})