Skip to content

getStructEncoder/Decoder/Codec do not infer a literal fixedSize from their fields #1738

@mcintyre94

Description

@mcintyre94

Note: minor, may not be feasible/realistic to fix. Opening as part of addressing #1443 in #1683. We've also discussed this idea before, so worth having an issue for it.

Summary

The fixed-size overloads of getStructEncoder, getStructDecoder, and getStructCodec type
their result as FixedSize*<T> - i.e. with the default TSize of number - rather than
inferring the literal byte size from the sum of their fields. Even when every field has a
statically-known literal size, the struct's fixedSize is typed as the broad number.

Current behaviour

import { getStructEncoder } from '@solana/codecs-data-structures';
import { getU32Encoder } from '@solana/codecs-numbers';

const e = getStructEncoder([['value', getU32Encoder()]]);
//    ^? FixedSizeEncoder<{ value: number }>   (size is `number`, not the literal `4`)

Compare to the U32Encoder:

const u32 = getU32Encoder();
//    ^? FixedSizeEncoder<number, 4>

Eg:

export function getStructEncoder<const TFields extends Fields<FixedSizeEncoder<any>>>(
fields: TFields,
): FixedSizeEncoder<GetEncoderTypeFromFields<TFields>>;

Why this matters

Size-aware combinators - getUnion*, getPredicate*, getPatternMatch* decide between a
FixedSize* and a VariableSize* result by comparing the literal sizes of their branches. When
two struct branches have different real sizes but both report fixedSize: number, these
combinators can't tell the sizes apart and conservatively widen to a plain Encoder/Decoder/
Codec. Consumers are then forced to cast. If we knew the sizes are different we could return VariableSize*.

Suggested fix

Add a TSize type parameter to the fixed-size overloads and compute it as the literal sum of the
field sizes (mirroring how the runtime already sums getFixedSize over the fields).

Caveat / open question / why this might not make sense

Type-level addition in TypeScript is pretty hacky. We might be able to do it when the struct is very small?

The motivating example is v1 transaction config where the struct actually only has one field:

function getCompiledTransactionConfigValueEncoder(): VariableSizeEncoder<CompiledTransactionConfigValue> {
return getPatternMatchEncoder<CompiledTransactionConfigValue>([
[value => value.kind === 'u32', getStructEncoder([['value', getU32Encoder()]])],
[value => value.kind === 'u64', getStructEncoder([['value', getU64Encoder()]])],
]) as unknown as VariableSizeEncoder<CompiledTransactionConfigValue>;
}

Acceptance criteria (typetest)

getStructEncoder([
    ['a', getU32Encoder()],
    ['b', getU8Encoder()],
]) satisfies FixedSizeEncoder<{ a: number; b: number }, 5>;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions