Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
SebastienGllmt opened this issue Nov 14, 2021 · 1 comment

Comments

@SebastienGllmt
Copy link

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

@SebastienGllmt
Copy link
Author

SebastienGllmt commented Nov 15, 2021

I suspect maybe this is part of a large issue, because for example

The following works

type Noop<Value> = Awaited<Promise<Value>>;

is<Noop<5>>(5) // true

However, the following doesn't

type Noop<Value> = {
    data: Awaited<Promise<Value>>
}

is<Noop<5>>({ data: 5 }) // throw an error

In this case, the failure case it will throw an error because if ((ts.TypeFlags.Conditional & type.flags) !== 0) isn't checked in the visitType or type-check or type-name, but it seems strange this would be an issue where as the version not wrapped in an object works perfectly and again, in the failure case, the target & aliasTypeArguments are at different levels in the stack

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant