0

I have an instance of my db user model and an array with a list of an object properties:

interface User = {
  firstName: string,
  lastName: string,
  isActive: boolean,
  phone: string,
  // and a lot of other extra properties omitted here
}

const selectedFields = ['firstName', 'lastName'];

I'm trying to use typescript generics to build a new type with the original class and only the selected properties:

const selectedUserInterface = {
  firstName: string,
  lastName: string,
}

I know I can use "as const" like described here ( TypeScript: Define a union type from an array of strings )

const fruits = ["Apple", "Orange", "Pear"] as const;
type Fruit = typeof fruits[number]; // "Apple" | "Orange" | "Pear"

But what I want is to write a generic type, something like an utility type, that return the same without using "as const" and without repeating "typeof ....".

I tried this but it's not working:

type SelectProps<T extends object, K extends (keyof T) & string[]> = Pick<T, K>;

As a stepback (just to obtain literal type from array) I tried this, but can't understand why it's not working

type ArrayToUnion<T extends readonly any[]> = typeof T[number];

Can anyone explain and guide me to the solution?

crivella
  • 514
  • 5
  • 19
  • 1
    Is this what you are looking for? https://tsplay.dev/mAgk4w If so, then I will write up that answer. If not then a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) will help a lot here to understand what you want. – Alex Wayne Jul 31 '23 at 20:51
  • It works! But is there a way to avoid repeating "typeof" all the time, including it inside the generic type? (instead of writing type TestA = SelectProps I'd like to write type TestA = SelectProps ) – crivella Jul 31 '23 at 21:01

1 Answers1

1

You almost had your type right.

You had this:

K extends (keyof T) & string[]

Which means that K must be a keyof T and also a string[]. But what you actually want is that K must be a (keyof T & string)[], that is an array where all members are a key of T and are also a string.

I think you want this:

type SelectProps<T, K extends readonly (keyof T)[]> = Pick<T, K[number]>;

You don't really need the extends object, or the & string here.


Now to make this work the type system must know what keys are provided. That means that this will not work:

const selectedFields = ['firstName', 'lastName'] // inferred as type: string[]

So you can as const there

const selectedFields = ['firstName', 'lastName'] as const
// inferred as type: readonly ['firstName', 'lastName']

Or you can use satisfies:

const selectedFields = ['firstName', 'lastName'] satisfies (keyof User)[]
// inferred as type: ('firstName' | 'lastName)[]

So now Typescript knows the prop names in the type system and can work with them.


is there a way to avoid repeating "typeof" all the time.

A value is data your code can use at runtime. A type is a constraint that describe the structure of your values. You can't use a value as a type. And how you use the type of a value is with the typeof operator. So, no.

But you can declare it once and save it as a type alias:

const selectedFields = ['firstName', 'lastName'] satisfies (keyof User)[]
type UserSelectedFields = typeof selectedFields

Putting all that together it looks like this:

interface User {
  firstName: string,
  lastName: string,
  isActive: boolean,
  phone: string,
  // and a lot of other extra properties omitted here
}

const selectedFields = ['firstName', 'lastName'] satisfies (keyof User)[]
type UserSelectedFields = typeof selectedFields

type SelectProps<T extends object, K extends (keyof T)[]> = Pick<T, K[number]>;

type TestA = SelectProps<User, UserSelectedFields>
//   ^? { firstName: string, lastName: string }

See Playground

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337