Skip to content

Parsing fails when target & aliasTypeArguments are at different levels #117

@SebastienGllmt

Description

@SebastienGllmt

Background

Consider the test case in PR #115

It works well when the use of the generic is in the top-level of the type. Notably, the visit order will look something like this

visitTypeAliasReference -> visitObjectType -> isIndexedAccessType -> visitTypeParameter -> success

Notice the first thing in the parsing order will be the aliasReference .

However, now consider the modified definition given below

export type StatusObject<Key extends keyof typeof Status> = number | {
    status: typeof Status[Key];
};

This modified code instead gives the following visit order

visitUnionOrIntersectionType
    -> visitNumber
    -> visitObjectType -> isIndexedAccessType -> visitTypeParameter -> error

Notice that the call to visitTypeAliasReference is missing and therefore the call to visitTypeParameter will fail.

Generated AST

If you look at the definition of the root type (the type representing the union), it has the following AST

TypeObject {
  checker: {
    ...
  },
  flags: 1048576,
  id: 115,
  objectFlags: 0,
  types: [
    // number type
    TypeObject {
      ...
    },
    // anonymous object type
    TypeObject {
      ...
    }
  ],
  aliasSymbol: SymbolObject {
    ...
  },
  // type argument for our generic
  aliasTypeArguments: [
    <ref *1> TypeObject {
      checker: [Object],
      flags: 128,
      id: 105,
      symbol: undefined,
      value: 'enable',
      regularType: [Circular *1],
      freshType: [TypeObject]
    }
  ]
}

Notably, note that the definition for aliasTypeArguments is not inside the object type, but rather annotated to the whole union expression. If you look at the AST for the anonymous object type, however, you will find the following:

<ref *2> TypeObject {
  checker: {
    ...
  },
  flags: 524288,
  id: 114,
  objectFlags: 201326672,
  symbol: SymbolObject {
    flags: 2048,
    escapedName: '__type',
    declarations: [ [NodeObject] ],
    members: Map(1) { 'status' => [SymbolObject] },
    id: 3758
  },
  members: undefined,
  properties: undefined,
  callSignatures: undefined,
  constructSignatures: undefined,
  stringIndexInfo: undefined,
  numberIndexInfo: undefined,
  target: <ref *1> TypeObject {
    checker: {
      ...
    },
    flags: 524288,
    id: 111,
    objectFlags: 201326608,
    symbol: SymbolObject {
      flags: 2048,
      escapedName: '__type',
      declarations: [Array],
      members: [Map],
      id: 3758
    },
    members: undefined,
    properties: undefined,
    callSignatures: undefined,
    constructSignatures: undefined,
    stringIndexInfo: undefined,
    numberIndexInfo: undefined,
    aliasSymbol: undefined,
    aliasTypeArguments: undefined,
    instantiations: Map(2) { '112' => [Circular *1], '105' => [Circular *2] }
  },
  mapper: {
    kind: 0,
    source: TypeObject {
      checker: [Object],
      flags: 262144,
      id: 112,
      symbol: [SymbolObject]
    },
    target: <ref *3> TypeObject {
      checker: [Object],
      flags: 128,
      id: 105,
      symbol: undefined,
      value: 'enable',
      regularType: [Circular *3],
      freshType: [TypeObject]
    }
  },
  aliasSymbol: undefined,
  aliasTypeArguments: undefined
}

Notice that both in its TypeObject and its target, there value for aliasTypeArguments is undefined.

This explains why visitTypeAliasReference is never called (recall: it only gets called if type.aliasTypeArguments && type.target which doesn't hold for our case).

Instead, we are provided with a mapper data structures that map our target to the aliasTypeArguments definition:

Can we use mapper?

mapper is not part of the public typescript API it seems, so we can't use it. It's slightly unfortunate because the usage is extremely simple

const mapping: Map<ts.Type, ts.Type> = new Map();
mapping.set(type.mapper.source, type.mapper.target);
visitorContext.typeMapperStack.push(mapping);

This mapper field is also present (but ignored) in other cases of AST parsing, so I'm not sure what the impact of using it here would be either.

Can we use instantiations?

This field also seems like in the general case it isn't part of the public API. It looks like it might enough information to rebuild what we want, but I'm not familiar enough with the AST to know if this is really the right approach.

Investigation conclusion

I'm not really sure where to go next on this one. It seems to tackle one of the assumptions on the Typescript AST used for this codebase so I'm not really sure if I should change it. The mapper solution works, but I'm not knowledgeable to know the implications of this solution or whether or not it's the right approach so I'd love any help on this

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions