danvk
danvk

Reputation: 16955

Can I force TypeScript to resolve the type of Pick<T, K> for display?

I have some TypeScript code that generates typed SQL queries using a schema produced by pg-to-ts:

const getUserByEmail = selectBy('user', ['email']);
// const getUserByEmail: (db: Queryable, select: Pick<User, "email">) => Promise<User[]>

This function works fine and has the correct type. But I don't like how it's displayed. Rather than showing select: Pick<User, "email"> in the parameters list, I'd like it to show select: {email: string}, which I find much clearer.

What's odd is that writing out Pick<User, "email"> in a type alias produces exactly the desired display when you mouse over it:

type ByEmail = Pick<User, 'email'>;
// type ByEmail = { email: string; }

So how can I match that display in my selectBy function? Here's a sketch of the implementation (full source is on the TypeScript playground):

export function selectBy<T extends TableNames, K extends keyof TableType<T>>(
  tableName: T,
  columns: K[],
) {
  type OutType = TableType<T>;
  type SelectType = Pick<OutType, K>;

  let query = `SELECT * FROM ${tableName} ...`;

  return async (db: Queryable, select: SelectType): Promise<OutType[]> =>
    (
      await db.query(
        query,
        columns.map(c => select[c]),
      )
    ).rows;
}

I'm aware of some of the tricks for controlling type display such as {} & and Exclude<K, never>, but I haven't been able to get them to work on this snippet.

Upvotes: 2

Views: 2515

Answers (1)

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21453

you can use a trivial NOP type that just maps every key of the type to the same value to force the intellisense features to show all properties.

In order to be functionally correct we need to leave function and constructor types alone and I think this check helps with the display more than Exclude<T,never> since typescript has to resolve the type enough to figure out it isn't a function and then by then it knows all the members so it can show them. I'm not 100% sure that is what is going on but I think it might be part of it.

playground

// maps any type to a trivial equivelent to the same type (handles functions and constructors correctly, there may be other types that are broken by homeomorphic mapped types)
type NOOP<T> = T extends (...args:any[])=>any ? T : T extends (abstract new (...args:any[])=>any) ? T : {[K in keyof T]: T[K]}

interface TEST{
  foo: 1
  bar: 2
  baz: 3
}

declare function f(test: Pick<TEST, 'foo'>): void
//                 ^? test: Pick<TEST,'foo'>

declare function g(test: NOOP<Pick<TEST,'foo'>>): void
//                  ^? test: {foo:1; }

Note that this is nearly equivalent to just checking if T extends Function but technically Function has a few methods that will also be checked so if the class defines a static property of call,apply, bind etc, it would break. (playground)

class W{
  static call = "a"
}
type AnyFunc = ((...args:any[])=>any) | (abstract new (...args:any[])=>any);
type NOP<T> =     T extends AnyFunc  ? T : {[K in keyof T]: T[K]}
type NOP_bad<T> = T extends Function ? T : {[K in keyof T]:T[K]}

const correctly_marked_as_error: NOP<typeof W> = {call:"a", prototype: new W()};

const allowed_but_probably_shouldnt: NOP_bad<typeof W> = {call:"a", prototype: new W()};

This is kind of a grey area in typescript where a call signature does type as though it has the expected function methods but with proxies and classes you can define non standard behaviour that breaks some of those assumptions.

Upvotes: 4

Related Questions