Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Created January 12, 2021 21:48
Show Gist options
  • Save ryanflorence/84114f38083a973e006787b9879886eb to your computer and use it in GitHub Desktop.
Save ryanflorence/84114f38083a973e006787b9879886eb to your computer and use it in GitHub Desktop.
generic generics?
// My DB has multiple collections (tables), they can extend
// this since the only difference is the data an collection,
// they're passed in as generics
export interface Record<TData, TCollection> {
data: TData;
ref: {
collection: {
id: TCollection;
};
id: string;
ts: number;
};
}
// for example, here are posts
interface PostRecord
extends Record<
{
title: string;
published: boolean;
body: string;
},
"posts"
> {}
// and sessions
interface SessionRecord
extends Record<
{
id: string;
values: { [key: string]: string };
},
"sessions"
> {}
// how do I tell this function it's getting a generic Record?
export function serializeRecord<
TRecord extends Record<???, ???>
>(record: TRecord): TRecord {
let { data, ref } = record;
return {
data,
ref: {
id: ref.id,
ts: ref.ts,
collection: {
id: ref.collection.id,
},
},
};
}
// is there some way to infer or access `TRecord`'s generic TData and TCollection?
@mariusschulz
Copy link

Your serializeRecord function could define two generic type parameters itself, TData and TCollection, and pass those through to the Record type:

export function serializeRecord<TData, TCollection>(
  record: Record<TData, TCollection>
): Record<TData, TCollection> {
  let { data, ref } = record;
  return {
    data,
    ref: {
      id: ref.id,
      ts: ref.ts,
      collection: {
        id: ref.collection.id,
      },
    },
  };
}

@ryanflorence
Copy link
Author

@mariusschulz That's what I wanted to avoid. I guess generics don't compose at all.

@mariusschulz
Copy link

@ryanflorence I'm not sure I follow. Could you elaborate in which ways composition is limited here?

@loucyx
Copy link

loucyx commented Jan 12, 2021

I used Thing instead of Record because Record is a type already in TS:

export interface Thing<Data = unknown, CollectionId = unknown> {
  data: Data;
  ref: {
    collection: {
      id: CollectionId;
    };
    id: string;
    ts: number;
  };
}

export const serializeRecord = <TRecord extends Thing>({
  data,
  ref
}: TRecord) =>
  ({
    data,
    ref: {
      id: ref.id,
      ts: ref.ts,
      collection: {
        id: ref.collection.id
      }
    }
  } as TRecord);

And that I think has the result you want:

type Post = Thing<
	{
		title: string;
		published: boolean;
		body: string;
	},
	"posts"
>;

let post: Post;

const test = serializeRecord(post); // test is of type Post

@DogPawHat
Copy link

Yeah, @mariusschulz has nailed it

@nandanmen
Copy link

Would assigning a default value to the generics work here? Something like:

export interface Record<TData = unknown, TCollection = unknown> {
  data: TData;
  ref: {
    collection: {
      id: TCollection;
    };
    id: string;
    ts: number;
  };
}

Which lets you write the function as:

export function serializeRecord<
  TRecord extends Record
>(record: TRecord): TRecord {
  let { data, ref } = record;
  return {
    data,
    ref: {
      id: ref.id,
      ts: ref.ts,
      collection: {
        id: ref.collection.id,
      },
    },
  };
}

@flq
Copy link

flq commented Jan 12, 2021

Check this out - it's a bit wild (it needs one cast, but it may be what you want if you care more about the consumer of that code)

export interface Envelope<TData = unknown, TCollection = unknown> {
  data: TData;
  ref: {
    collection: {
      id: TCollection;
    };
    id: string;
    ts: number;
  };
}

interface SessionRecord
  extends Envelope<
    {
      id: string;
      values: { [key: string]: string };
    },
    "sessions"
  > {}

type Inferrer<X> = X extends Envelope<infer T1, infer T2> ? Envelope<T1,T2> : never;

export function serializeRecord<
  TRecord extends Envelope
>(record: TRecord): Inferrer<TRecord> {
  let { data, ref } = record;
  return {
    data,
    ref: {
      id: ref.id,
      ts: ref.ts,
      collection: {
        id: ref.collection.id,
      },
    },
  } as Inferrer<TRecord>;
}

const output = serializeRecord<SessionRecord>("foo" as any);

The Inferrer helper will squeeze your 2 type arguments back into the return of the function without you specifying it (at least in my VS Code session :D)

@ryanflorence
Copy link
Author

I went with:

interface GenericRecord extends IRecord<any, string> {}

export function serializeRecord<TRecord>(record: GenericRecord): TRecord {
  let { data, ref } = record;
  return ({
    data,
    ref: {
      id: ref.id,
      ts: ref.ts,
      collection: {
        id: ref.collection.id,
      },
    },
  } as unknown) as TRecord;
}

Ya'll have given me a lot read up on! Thank you :)

@Paduado
Copy link

Paduado commented Jan 13, 2021

Why not:

export function serializeRecord<
  T extends Record<any, string>
>(record: T): T {
  let { data, ref } = record;
  return {
    data,
    ref: {
      id: ref.id,
      ts: ref.ts,
      collection: {
        id: ref.collection.id,
      },
    },
  } as T;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment