Skip to content

Commit

Permalink
fix: incorrect serialization of IndexSignature and Record (#5211)
Browse files Browse the repository at this point in the history
  • Loading branch information
microdev1 committed Dec 25, 2023
1 parent a781c2a commit b005dc6
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 23 deletions.
81 changes: 58 additions & 23 deletions packages/server/src/shared/internal/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,48 +40,83 @@ export type Serialize<T> =

/** JSON serialize [tuples](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) */
type SerializeTuple<T extends [unknown, ...unknown[]]> = {
[k in keyof T]: T[k] extends NonJsonPrimitive ? null : Serialize<T[k]>;
[K in keyof T]: T[K] extends NonJsonPrimitive ? null : Serialize<T[K]>;
};

// prettier-ignore
type SerializeObjectKey<T extends Record<any, any>, TKey> =
type SerializeObjectKey<T extends Record<any, any>, K> =
// never include entries where the key is a symbol
TKey extends symbol ? never :
K extends symbol ? never :
// always include entries where the value is any
IsAny<T[TKey]> extends true ? TKey :
IsAny<T[K]> extends true ? K :
// always include entries where the value is unknown
unknown extends T[TKey] ? TKey :
unknown extends T[K] ? K :
// never include entries where the value is a non-JSON primitive
T[TKey] extends NonJsonPrimitive ? never :
T[K] extends NonJsonPrimitive ? never :
// otherwise serialize the value
TKey;
K;
/**
* JSON serialize objects (not including arrays) and classes
* @internal
**/
export type SerializeObject<T extends object> = {
[$Key in keyof T as SerializeObjectKey<T, $Key>]: Serialize<T[$Key]>;
[K in keyof T as SerializeObjectKey<T, K>]: Serialize<T[K]>;
};

type FilterDefinedKeys<TObj extends object> = Exclude<
/**
* Extract keys from T where the value dosen't extend undefined
* Note: Can't parse IndexSignature or Record types
*/
type FilterDefinedKeys<T extends object> = Exclude<
{
[TKey in keyof TObj]: undefined extends TObj[TKey] ? never : TKey;
}[keyof TObj],
[K in keyof T]: undefined extends T[K] ? never : K;
}[keyof T],
undefined
>;

/*
* For an object T, if it has any properties that are a union with `undefined`,
* make those into optional properties instead.
*
* Example: { a: string | undefined} --> { a?: string}
/**
* Get value of exactOptionalPropertyTypes config
*/
type ExactOptionalPropertyTypes = { a?: 0 | undefined } extends {
a?: 0;
}
? false
: true;

/**
* Check if T has an index signature
*/
type HasIndexSignature<T extends object> = string extends keyof T
? true
: false;

/**
* { [key: string]: number | undefined } --> { [key: string]: number }
*/
type HandleIndexSignature<T extends object> = {
[K in keyof Omit<T, keyof WithoutIndexSignature<T>>]: Exclude<
T[K],
undefined
>;
};

/**
* { a: number | undefined } --> { a?: number }
* Note: Can't parse IndexSignature or Record types
*/
type HandleUndefined<T extends object> = {
[K in keyof Omit<T, FilterDefinedKeys<T>>]?: Exclude<T[K], undefined>;
};

/**
* Handle undefined, index signature and records
*/
type UndefinedToOptional<T extends object> =
// Property is not a union with `undefined`, keep as-is
Pick<
WithoutIndexSignature<T>,
FilterDefinedKeys<WithoutIndexSignature<T>>
> & {
// Property _is_ a union with `defined`. Set as optional (via `?`) and remove `undefined` from the union
[k in keyof Omit<T, FilterDefinedKeys<T>>]?: Exclude<T[k], undefined>;
};
Pick<WithoutIndexSignature<T>, FilterDefinedKeys<WithoutIndexSignature<T>>> &
// If following is true, don't merge undefined or optional into index signature if any in T
(ExactOptionalPropertyTypes extends true
? HandleIndexSignature<T> & HandleUndefined<WithoutIndexSignature<T>>
: HasIndexSignature<T> extends true
? HandleIndexSignature<T>
: HandleUndefined<T>);
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { inferRouterOutputs, initTRPC } from '@trpc/server';

const t = initTRPC.create();

const appRouter = t.router({
outputWithIndexSignature: t.procedure.query(() => {
return {} as {
a: number;
b: number;
[c: string]: number;
};
}),

outputWithRecord: t.procedure.query(() => {
return {} as {
a: number;
b: number;
} & Record<string, number>;
}),

outputWithRecordAndIndexSignature: t.procedure.query(() => {
return {} as {
a: number;
b: number;
[c: string]: number;
} & Record<string, number>;
}),

outputWithUndefinedAndUndefinedIndexSignature: t.procedure.query(() => {
return {} as {
a: number;
b: number | undefined;
[c: string]: number | undefined;
};
}),

outputWithUndefinedAndUndefinedRecord: t.procedure.query(() => {
return {} as {
a: number;
b: number | undefined;
} & Record<string, number | undefined>;
}),

outputWithUndefinedAndUndefinedRecordAndUndefinedIndexSignature:
t.procedure.query(() => {
return {} as {
a: number;
b: number | undefined;
[c: string]: number | undefined;
} & Record<string, number | undefined>;
}),

outputWithUndefinedIndexSignature: t.procedure.query(() => {
return {} as {
a: number;
b: number;
[c: string]: number | undefined;
};
}),

outputWithUndefinedRecord: t.procedure.query(() => {
return {} as {
a: number;
b: number;
} & Record<string, number | undefined>;
}),

outputWithUndefinedRecordAndUndefinedIndexSignature: t.procedure.query(() => {
return {} as {
a: number;
b: number;
[c: string]: number | undefined;
} & Record<string, number | undefined>;
}),
});

type AppRouter = typeof appRouter;

describe('inferRouterOutputs', () => {
type AppRouterOutputs = inferRouterOutputs<AppRouter>;

test('outputWithIndexSignature', () => {
type Output = AppRouterOutputs['outputWithIndexSignature'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
[x: number]: number;
a: number;
b: number;
}>();
});

test('outputWithRecord', () => {
type Output = AppRouterOutputs['outputWithRecord'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
a: number;
b: number;
}>();
});

test('outputWithRecordAndIndexSignature', () => {
type Output = AppRouterOutputs['outputWithRecordAndIndexSignature'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
[x: number]: number;
a: number;
b: number;
}>();
});

test('outputWithUndefinedAndUndefinedIndexSignature', () => {
type Output =
AppRouterOutputs['outputWithUndefinedAndUndefinedIndexSignature'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
[x: number]: number;
a: number;
}>();
});

test('outputWithUndefinedAndRecord', () => {
type Output = AppRouterOutputs['outputWithUndefinedAndUndefinedRecord'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
a: number;
}>();
});

test('outputWithUndefinedAndRecordAndIndexSignature', () => {
type Output =
AppRouterOutputs['outputWithUndefinedAndUndefinedRecordAndUndefinedIndexSignature'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
[x: number]: number;
a: number;
}>();
});

test('outputWithUndefinedIndexSignature', () => {
type Output = AppRouterOutputs['outputWithUndefinedIndexSignature'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
[x: number]: number;
a: number;
b: number;
}>();
});

test('outputWithUndefinedRecord', () => {
type Output = AppRouterOutputs['outputWithUndefinedRecord'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
a: number;
b: number;
}>();
});

test('outputWithUndefinedRecordAndUndefinedIndexSignature', () => {
type Output =
AppRouterOutputs['outputWithUndefinedRecordAndUndefinedIndexSignature'];
expectTypeOf<Output>().toEqualTypeOf<{
[x: string]: number;
[x: number]: number;
a: number;
b: number;
}>();
});
});

3 comments on commit b005dc6

@vercel
Copy link

@vercel vercel bot commented on b005dc6 Dec 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-prisma-starter – ./examples/next-prisma-starter

next-prisma-starter-trpc.vercel.app
next-prisma-starter-git-main-trpc.vercel.app
nextjs.trpc.io

@vercel
Copy link

@vercel vercel bot commented on b005dc6 Dec 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

og-image – ./www/og-image

og-image-trpc.vercel.app
og-image-three-neon.vercel.app
og-image.trpc.io
og-image-git-main-trpc.vercel.app

@vercel
Copy link

@vercel vercel bot commented on b005dc6 Dec 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

www – ./www

www-trpc.vercel.app
beta.trpc.io
www-git-main-trpc.vercel.app
www.trpc.io
trpc.io
alpha.trpc.io

Please sign in to comment.