Skip to content

Fails to propagate returned generic type as argument of another function #51925

@TruffeCendree

Description

@TruffeCendree

Bug Report

In the following example, wrongInference.properties.account inferred as unknown. I was hoping it would be inferred the same way as wellInferred.

// partialInferred.properties.name inferred as string, no other property as expected
const partialInferred = pickFromSchema(accountSchema, 'name')

// wellInfered.properties.account is well inferred
const wellInferred = propertiesToObjectSchema({ account: partialInferred })

// wellInferredAlternative.properties.account is well inferred
const wellInferredAlternative = {
  type: 'object',
  required: ['name'],
  additionalProperties: false,
  properties: pickFromSchema(accountSchema, 'name')
} as const

// wrongInferrence.properties.account inferred as `unknown`
const wrongInferrence = propertiesToObjectSchema({ account: pickFromSchema(accountSchema, 'name') })

I suppose this is a built-in mecanism to prevent too many recursions in typescript compiler.

🔎 Search Terms

  • "typescript type propagation in nested function calls"
  • "typescript max type recursion"
  • "typescript wrongly inferred as unknown"

🕗 Version & Regression Information

I tried various versions in the playground UI, without finding one valid.
I am currently using version 4.9.4.

⏯ Playground Link

Full example available on playground.

💻 Code

interface BaseObjectSchema<Properties extends Record<string, unknown>> {
  type: 'object'
  properties: Properties
  additionalProperties?: boolean
  required?: ReadonlyArray<keyof Properties> | Array<keyof Properties>
}

/**
 * Returns a new schema that picks some of the properties of base schema.
 * Those properties are marked as required in the resulting schema.
 */
function pickFromSchema<
  Properties extends Record<string, unknown>,
  SubsetPropertyName extends keyof Properties & string,
  Return = Required<
    BaseObjectSchema<{
      [PropertyName in SubsetPropertyName]: PropertyName extends keyof Properties ? Properties[PropertyName] : never
    }>
  >
>(schema: BaseObjectSchema<Properties>, ...properties: SubsetPropertyName[]) {
  const newSchema = {
    type: 'object' as const,
    properties: {} as Record<string, unknown>,
    additionalProperties: false,
    required: properties
  }

  for (const property of properties) newSchema.properties[property] = schema.properties[property]
  return newSchema as Return
}

/**
 * Builds a new schema from a literal object, those keys are JSON schemas.
 * Some properties can be set as optional using opts.optional.
 */
function propertiesToObjectSchema<Properties extends Record<string, unknown>>(
  properties: Properties,
  opts: { optional?: (keyof Properties)[] } = {}
): Required<BaseObjectSchema<Properties>> {
  return {
    type: 'object' as const,
    properties,
    required: (Object.keys(properties) as (keyof Properties)[]).filter(key => !opts.optional?.includes(key)),
    additionalProperties: false
  }
}

const accountSchema = propertiesToObjectSchema({
  id: { type: 'string' },
  name: { type: 'string' },
  createdAt: { type: 'string', format: 'date-time' },
  updatedAt: { type: 'string', format: 'date-time' }
} as const)

// partialInferred.properties.name infered as string, no other property as expected
const partialInferred = pickFromSchema(accountSchema, 'name')

// wellInfered.properties.account is well infered
const wellInferred = propertiesToObjectSchema({ account: partialInferred })

// wellInferredAlternative.properties.account is well infered
const wellInferredAlternative = {
  type: 'object',
  required: ['name'],
  additionalProperties: false,
  properties: pickFromSchema(accountSchema, 'name')
} as const

// wrongInferrence.properties.account infered as `unknown`
const wrongInferrence = propertiesToObjectSchema({ account: pickFromSchema(accountSchema, 'name') })

🙁 Actual behavior

wrongInferrence.properties.account inferred as unknown

🙂 Expected behavior

Expecting wrongInferrence to have the same type as wellInferred and wellInferredAlternative,

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs InvestigationThis issue needs a team member to investigate its status.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions