Skip to content

Commit

Permalink
refactor(experimental): improve getTupleCodec type inferences (#2344)
Browse files Browse the repository at this point in the history
This PR follows the footsteps of #2181 which improved the type inferences of `getStructCodec` and `getDataEnumCodec` by inferring the types from the provided codecs instead of inferring the other way around.

This PR does the same with the `getTupleCodec` and also makes use of the `DrainOuterGeneric` helper from the previous PR to reduce the amount of instantiations TypeScript does when resolving the codec types.
  • Loading branch information
lorisleiva committed Mar 20, 2024
1 parent 6dcf548 commit deb7b80
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 57 deletions.
7 changes: 7 additions & 0 deletions .changeset/green-experts-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@solana/codecs-data-structures': patch
---

Improve `getTupleCodec` type inferences and performance

The tuple codec now infers its encoded/decoded type from the provided codec array and uses the new `DrainOuterGeneric` helper to reduce the number of TypeScript instantiations.
10 changes: 5 additions & 5 deletions packages/codecs-data-structures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,16 @@ const map = getMapDecoder(keyDecoder, valueDecoder).decode(bytes);
The `getTupleCodec` function accepts any number of codecs — `T`, `U`, `V`, etc. — and returns a tuple codec of type `[T, U, V, …]` such that each item is in the order of the provided codecs.

```ts
const itemCodecs = [getStringCodec(), getU8Codec(), getU64Codec()];
const bytes = getTupleCodec(itemCodecs).encode(['alice', 42, 123]);
const tuple = getTupleCodec(itemCodecs).decode(bytes);
const codec = getTupleCodec([getStringCodec(), getU8Codec(), getU64Codec()]);
const bytes = codec.encode(['alice', 42, 123]);
const tuple = codec.decode(bytes);
```

Separate `getTupleEncoder` and `getTupleDecoder` functions are also available.

```ts
const bytes = getTupleEncoder(itemEncoders).encode(['alice', 42, 123]);
const tuple = getTupleDecoder(itemDecoders).decode(bytes);
const bytes = getTupleEncoder([getStringCodec(), getU8Codec()]).encode(['alice', 42]);
const tuple = getTupleDecoder([getStringCodec(), getU8Codec()]).decode(bytes);
```

## Struct codec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('getTupleCodec', () => {
expect(tuple([string(), u8()]).decode(b('0500000048656c6c6f2a'))).toStrictEqual(['Hello', 42]);

// Different From and To types.
const tupleU8U64 = tuple<[number, bigint | number], [number, bigint]>([u8(), u64()]);
const tupleU8U64 = tuple([u8(), u64()]);
expect(tupleU8U64.encode([1, 2])).toStrictEqual(b('010200000000000000'));
expect(tupleU8U64.encode([1, 2n])).toStrictEqual(b('010200000000000000'));
expect(tupleU8U64.decode(b('010200000000000000'))).toStrictEqual([1, 2n]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {
Codec,
Decoder,
Encoder,
FixedSizeCodec,
FixedSizeDecoder,
FixedSizeEncoder,
Expand All @@ -13,21 +16,44 @@ import { getTupleCodec, getTupleDecoder, getTupleEncoder } from '../tuple';

{
// [getTupleEncoder]: It knows if the encoder is fixed size or variable size.
getTupleEncoder([]) satisfies FixedSizeEncoder<[]>;
getTupleEncoder([getU8Encoder()]) satisfies FixedSizeEncoder<[number]>;
getTupleEncoder([getU8Encoder(), getUtf8Encoder()]) satisfies VariableSizeEncoder<[number, string]>;
getTupleEncoder([]) satisfies FixedSizeEncoder<readonly []>;
getTupleEncoder([getU8Encoder()]) satisfies FixedSizeEncoder<readonly [number]>;
getTupleEncoder([getU8Encoder(), getUtf8Encoder()]) satisfies VariableSizeEncoder<readonly [number, string]>;
}

{
// [getTupleEncoder]: It infers the correct tuple type from the encoders.
getTupleEncoder([getU8Encoder(), getUtf8Encoder()]) satisfies Encoder<readonly [number, string]>;
// @ts-expect-error It does not combine all items into a single union type.
getTupleEncoder([getU8Encoder(), getUtf8Encoder()]) satisfies Encoder<readonly (number | string)[]>;
}

{
// [getTupleDecoder]: It knows if the decoder is fixed size or variable size.
getTupleDecoder([]) satisfies FixedSizeDecoder<[]>;
getTupleDecoder([getU8Decoder()]) satisfies FixedSizeDecoder<[number]>;
getTupleDecoder([getU8Decoder(), getUtf8Decoder()]) satisfies VariableSizeDecoder<[number, string]>;
getTupleDecoder([]) satisfies FixedSizeDecoder<readonly []>;
getTupleDecoder([getU8Decoder()]) satisfies FixedSizeDecoder<readonly [number]>;
getTupleDecoder([getU8Decoder(), getUtf8Decoder()]) satisfies VariableSizeDecoder<readonly [number, string]>;
}

{
// [getTupleDecoder]: It infers the correct tuple type from the decoders.
getTupleDecoder([getU8Decoder(), getUtf8Decoder()]) satisfies Decoder<readonly [number, string]>;
// Because decoder types are returned (and not requested), Decoder<[number, string]> does satisfy Decoder<(number | string)[]>.
getTupleDecoder([getU8Decoder(), getUtf8Decoder()]) satisfies Decoder<readonly (number | string)[]>;
// @ts-expect-error However, we cannot do things like swapping the order of the types.
getTupleDecoder([getU8Decoder(), getUtf8Decoder()]) satisfies Decoder<readonly [string, number]>;
}

{
// [getTupleCodec]: It knows if the codec is fixed size or variable size.
getTupleCodec([]) satisfies FixedSizeCodec<[]>;
getTupleCodec([getU8Codec()]) satisfies FixedSizeCodec<[number]>;
getTupleCodec([getU8Codec(), getUtf8Codec()]) satisfies VariableSizeCodec<[number, string]>;
getTupleCodec([]) satisfies FixedSizeCodec<readonly []>;
getTupleCodec([getU8Codec()]) satisfies FixedSizeCodec<readonly [number]>;
getTupleCodec([getU8Codec(), getUtf8Codec()]) satisfies VariableSizeCodec<readonly [number, string]>;
}

{
// [getTupleCodec]: It infers the correct tuple type from the codecs.
getTupleCodec([getU8Codec(), getUtf8Codec()]) satisfies Codec<readonly [number, string]>;
// @ts-expect-error It does not combine all items into a single union type.
getTupleCodec([getU8Codec(), getUtf8Codec()]) satisfies Codec<readonly (number | string)[]>;
}
2 changes: 1 addition & 1 deletion packages/codecs-data-structures/src/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function getMapDecoder<TToKey, TToValue>(
config: MapCodecConfig<NumberDecoder> = {},
): Decoder<Map<TToKey, TToValue>> {
return mapDecoder(
getArrayDecoder(getTupleDecoder([key, value]), config as object),
getArrayDecoder(getTupleDecoder([key, value]), config as object) as Decoder<[TToKey, TToValue][]>,
(entries: [TToKey, TToValue][]): Map<TToKey, TToValue> => new Map(entries),
);
}
Expand Down
85 changes: 44 additions & 41 deletions packages/codecs-data-structures/src/tuple.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Codec,
combineCodec,
Expand All @@ -15,40 +16,31 @@ import {
} from '@solana/codecs-core';

import { assertValidNumberOfItemsForCodec } from './assertions';
import { getFixedSize, getMaxSize, sumCodecSizes } from './utils';
import { DrainOuterGeneric, getFixedSize, getMaxSize, sumCodecSizes } from './utils';

type WrapInFixedSizeEncoder<TFrom> = {
[P in keyof TFrom]: FixedSizeEncoder<TFrom[P]>;
};
type WrapInEncoder<TFrom> = {
[P in keyof TFrom]: Encoder<TFrom[P]>;
};
type WrapInFixedSizeDecoder<TTo> = {
[P in keyof TTo]: FixedSizeDecoder<TTo[P]>;
};
type WrapInDecoder<TTo> = {
[P in keyof TTo]: Decoder<TTo[P]>;
};
type WrapInCodec<TFrom, TTo extends TFrom> = {
[P in keyof TFrom]: Codec<TFrom[P], TTo[P]>;
};
type WrapInFixedSizeCodec<TFrom, TTo extends TFrom> = {
[P in keyof TFrom]: FixedSizeCodec<TFrom[P], TTo[P]>;
};
type GetEncoderTypeFromItems<TItems extends readonly Encoder<any>[]> = DrainOuterGeneric<{
[I in keyof TItems]: TItems[I] extends Encoder<infer TFrom> ? TFrom : never;
}>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyArray = any[];
type GetDecoderTypeFromItems<TItems extends readonly Decoder<any>[]> = DrainOuterGeneric<{
[I in keyof TItems]: TItems[I] extends Decoder<infer TTo> ? TTo : never;
}>;

/**
* Creates a encoder for a tuple-like array.
*
* @param items - The encoders to use for each item in the tuple.
*/
export function getTupleEncoder<TFrom extends AnyArray>(
items: WrapInFixedSizeEncoder<[...TFrom]>,
): FixedSizeEncoder<TFrom>;
export function getTupleEncoder<TFrom extends AnyArray>(items: WrapInEncoder<[...TFrom]>): VariableSizeEncoder<TFrom>;
export function getTupleEncoder<TFrom extends AnyArray>(items: WrapInEncoder<[...TFrom]>): Encoder<TFrom> {
export function getTupleEncoder<const TItems extends readonly FixedSizeEncoder<any>[]>(
items: TItems,
): FixedSizeEncoder<GetEncoderTypeFromItems<TItems>>;
export function getTupleEncoder<const TItems extends readonly Encoder<any>[]>(
items: TItems,
): VariableSizeEncoder<GetEncoderTypeFromItems<TItems>>;
export function getTupleEncoder<const TItems extends readonly Encoder<any>[]>(
items: TItems,
): Encoder<GetEncoderTypeFromItems<TItems>> {
type TFrom = GetEncoderTypeFromItems<TItems>;
const fixedSize = sumCodecSizes(items.map(getFixedSize));
const maxSize = sumCodecSizes(items.map(getMaxSize)) ?? undefined;

Expand All @@ -75,16 +67,24 @@ export function getTupleEncoder<TFrom extends AnyArray>(items: WrapInEncoder<[..
*
* @param items - The decoders to use for each item in the tuple.
*/
export function getTupleDecoder<TTo extends AnyArray>(items: WrapInFixedSizeDecoder<[...TTo]>): FixedSizeDecoder<TTo>;
export function getTupleDecoder<TTo extends AnyArray>(items: WrapInDecoder<[...TTo]>): VariableSizeDecoder<TTo>;
export function getTupleDecoder<TTo extends AnyArray>(items: WrapInDecoder<[...TTo]>): Decoder<TTo> {

export function getTupleDecoder<const TItems extends readonly FixedSizeDecoder<any>[]>(
items: TItems,
): FixedSizeDecoder<GetDecoderTypeFromItems<TItems>>;
export function getTupleDecoder<const TItems extends readonly Decoder<any>[]>(
items: TItems,
): VariableSizeDecoder<GetDecoderTypeFromItems<TItems>>;
export function getTupleDecoder<const TItems extends readonly Decoder<any>[]>(
items: TItems,
): Decoder<GetDecoderTypeFromItems<TItems>> {
type TTo = GetDecoderTypeFromItems<TItems>;
const fixedSize = sumCodecSizes(items.map(getFixedSize));
const maxSize = sumCodecSizes(items.map(getMaxSize)) ?? undefined;

return createDecoder({
...(fixedSize === null ? { maxSize } : { fixedSize }),
read: (bytes: Uint8Array, offset) => {
const values = [] as AnyArray as TTo;
const values = [] as Array<any> & TTo;
items.forEach(item => {
const [newValue, newOffset] = item.read(bytes, offset);
values.push(newValue);
Expand All @@ -100,17 +100,20 @@ export function getTupleDecoder<TTo extends AnyArray>(items: WrapInDecoder<[...T
*
* @param items - The codecs to use for each item in the tuple.
*/
export function getTupleCodec<TFrom extends AnyArray, TTo extends TFrom = TFrom>(
items: WrapInFixedSizeCodec<[...TFrom], [...TTo]>,
): FixedSizeCodec<TFrom, TTo>;
export function getTupleCodec<TFrom extends AnyArray, TTo extends TFrom = TFrom>(
items: WrapInCodec<[...TFrom], [...TTo]>,
): VariableSizeCodec<TFrom, TTo>;
export function getTupleCodec<TFrom extends AnyArray, TTo extends TFrom = TFrom>(
items: WrapInCodec<[...TFrom], [...TTo]>,
): Codec<TFrom, TTo> {
export function getTupleCodec<const TItems extends readonly FixedSizeCodec<any>[]>(
items: TItems,
): FixedSizeCodec<GetEncoderTypeFromItems<TItems>, GetDecoderTypeFromItems<TItems> & GetEncoderTypeFromItems<TItems>>;
export function getTupleCodec<const TItems extends readonly Codec<any>[]>(
items: TItems,
): VariableSizeCodec<
GetEncoderTypeFromItems<TItems>,
GetDecoderTypeFromItems<TItems> & GetEncoderTypeFromItems<TItems>
>;
export function getTupleCodec<const TItems extends readonly Codec<any>[]>(
items: TItems,
): Codec<GetEncoderTypeFromItems<TItems>, GetDecoderTypeFromItems<TItems> & GetEncoderTypeFromItems<TItems>> {
return combineCodec(
getTupleEncoder(items as WrapInEncoder<[...TFrom]>),
getTupleDecoder(items as WrapInDecoder<[...TTo]>),
getTupleEncoder(items),
getTupleDecoder(items) as Decoder<GetDecoderTypeFromItems<TItems> & GetEncoderTypeFromItems<TItems>>,
);
}

0 comments on commit deb7b80

Please sign in to comment.