})
})
+ describe('generics', () => {
+ test('generic with type literal', () => {
+ expect(
+ resolve(`
+ type Props<T> = T
+ defineProps<Props<{ foo: string }>>()
+ `).props
+ ).toStrictEqual({
+ foo: ['String']
+ })
+ })
+
+ test('generic used in intersection', () => {
+ expect(
+ resolve(`
+ type Foo = { foo: string; }
+ type Bar = { bar: number; }
+ type Props<T,U> = T & U & { baz: boolean }
+ defineProps<Props<Foo, Bar>>()
+ `).props
+ ).toStrictEqual({
+ foo: ['String'],
+ bar: ['Number'],
+ baz: ['Boolean']
+ })
+ })
+
+ test('generic type /w generic type alias', () => {
+ expect(
+ resolve(`
+ type Aliased<T> = Readonly<Partial<T>>
+ type Props<T> = Aliased<T>
+ type Foo = { foo: string; }
+ defineProps<Props<Foo>>()
+ `).props
+ ).toStrictEqual({
+ foo: ['String']
+ })
+ })
+
+ test('generic type /w aliased type literal', () => {
+ expect(
+ resolve(`
+ type Aliased<T> = { foo: T }
+ defineProps<Aliased<string>>()
+ `).props
+ ).toStrictEqual({
+ foo: ['String']
+ })
+ })
+
+ test('generic type /w interface', () => {
+ expect(
+ resolve(`
+ interface Props<T> {
+ foo: T
+ }
+ type Foo = string
+ defineProps<Props<Foo>>()
+ `).props
+ ).toStrictEqual({
+ foo: ['String']
+ })
+ })
+
+ test('generic from external-file', () => {
+ const files = {
+ '/foo.ts': 'export type P<T> = { foo: T }'
+ }
+ const { props } = resolve(
+ `
+ import { P } from './foo'
+ defineProps<P<string>>()
+ `,
+ files
+ )
+ expect(props).toStrictEqual({
+ foo: ['String']
+ })
+ })
+ })
+
describe('external type imports', () => {
test('relative ts', () => {
const files = {
export function resolveTypeElements(
ctx: TypeResolveContext,
node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
- scope?: TypeScope
+ scope?: TypeScope,
+ typeParameters?: Record<string, Node>
): ResolvedElements {
if (node._resolvedElements) {
return node._resolvedElements
return (node._resolvedElements = innerResolveTypeElements(
ctx,
node,
- node._ownerScope || scope || ctxToScope(ctx)
+ node._ownerScope || scope || ctxToScope(ctx),
+ typeParameters
))
}
function innerResolveTypeElements(
ctx: TypeResolveContext,
node: Node,
- scope: TypeScope
+ scope: TypeScope,
+ typeParameters?: Record<string, Node>
): ResolvedElements {
switch (node.type) {
case 'TSTypeLiteral':
- return typeElementsToMap(ctx, node.members, scope)
+ return typeElementsToMap(ctx, node.members, scope, typeParameters)
case 'TSInterfaceDeclaration':
- return resolveInterfaceMembers(ctx, node, scope)
+ return resolveInterfaceMembers(ctx, node, scope, typeParameters)
case 'TSTypeAliasDeclaration':
case 'TSParenthesizedType':
- return resolveTypeElements(ctx, node.typeAnnotation, scope)
+ return resolveTypeElements(
+ ctx,
+ node.typeAnnotation,
+ scope,
+ typeParameters
+ )
case 'TSFunctionType': {
return { props: {}, calls: [node] }
}
case 'TSUnionType':
case 'TSIntersectionType':
return mergeElements(
- node.types.map(t => resolveTypeElements(ctx, t, scope)),
+ node.types.map(t => resolveTypeElements(ctx, t, scope, typeParameters)),
node.type
)
case 'TSMappedType':
scope.imports[typeName]?.source === 'vue'
) {
return resolveExtractPropTypes(
- resolveTypeElements(ctx, node.typeParameters.params[0], scope),
+ resolveTypeElements(
+ ctx,
+ node.typeParameters.params[0],
+ scope,
+ typeParameters
+ ),
scope
)
}
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
- return resolveTypeElements(ctx, resolved, resolved._ownerScope)
+ const typeParams: Record<string, Node> = Object.create(null)
+ if (
+ (resolved.type === 'TSTypeAliasDeclaration' ||
+ resolved.type === 'TSInterfaceDeclaration') &&
+ resolved.typeParameters &&
+ node.typeParameters
+ ) {
+ resolved.typeParameters.params.forEach((p, i) => {
+ let param = typeParameters && typeParameters[p.name]
+ if (!param) param = node.typeParameters!.params[i]
+ typeParams[p.name] = param
+ })
+ }
+ return resolveTypeElements(
+ ctx,
+ resolved,
+ resolved._ownerScope,
+ typeParams
+ )
} else {
if (typeof typeName === 'string') {
+ if (typeParameters && typeParameters[typeName]) {
+ return resolveTypeElements(
+ ctx,
+ typeParameters[typeName],
+ scope,
+ typeParameters
+ )
+ }
if (
// @ts-ignore
SupportedBuiltinsSet.has(typeName)
) {
- return resolveBuiltin(ctx, node, typeName as any, scope)
+ return resolveBuiltin(
+ ctx,
+ node,
+ typeName as any,
+ scope,
+ typeParameters
+ )
} else if (typeName === 'ReturnType' && node.typeParameters) {
// limited support, only reference types
const ret = resolveReturnType(
function typeElementsToMap(
ctx: TypeResolveContext,
elements: TSTypeElement[],
- scope = ctxToScope(ctx)
+ scope = ctxToScope(ctx),
+ typeParameters?: Record<string, Node>
): ResolvedElements {
const res: ResolvedElements = { props: {} }
for (const e of elements) {
if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
+ // capture generic parameters on node's scope
+ if (typeParameters) {
+ scope = createChildScope(scope)
+ Object.assign(scope.types, typeParameters)
+ }
;(e as MaybeWithScope)._ownerScope = scope
const name = getId(e.key)
if (name && !e.computed) {
function resolveInterfaceMembers(
ctx: TypeResolveContext,
node: TSInterfaceDeclaration & MaybeWithScope,
- scope: TypeScope
+ scope: TypeScope,
+ typeParameters?: Record<string, Node>
): ResolvedElements {
- const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
+ const base = typeElementsToMap(
+ ctx,
+ node.body.body,
+ node._ownerScope,
+ typeParameters
+ )
if (node.extends) {
for (const ext of node.extends) {
if (
ctx: TypeResolveContext,
node: TSTypeReference | TSExpressionWithTypeArguments,
name: GetSetType<typeof SupportedBuiltinsSet>,
- scope: TypeScope
+ scope: TypeScope,
+ typeParameters?: Record<string, Node>
): ResolvedElements {
- const t = resolveTypeElements(ctx, node.typeParameters!.params[0], scope)
+ const t = resolveTypeElements(
+ ctx,
+ node.typeParameters!.params[0],
+ scope,
+ typeParameters
+ )
switch (name) {
case 'Partial': {
const res: ResolvedElements = { props: {}, calls: t.calls }
return node._resolvedChildScope
}
- const scope = new TypeScope(
- parentScope.filename,
- parentScope.source,
- parentScope.offset,
- Object.create(parentScope.imports),
- Object.create(parentScope.types),
- Object.create(parentScope.declares)
- )
+ const scope = createChildScope(parentScope)
if (node.body.type === 'TSModuleDeclaration') {
const decl = node.body as TSModuleDeclaration & WithScope
return (node._resolvedChildScope = scope)
}
+function createChildScope(parentScope: TypeScope) {
+ return new TypeScope(
+ parentScope.filename,
+ parentScope.source,
+ parentScope.offset,
+ Object.create(parentScope.imports),
+ Object.create(parentScope.types),
+ Object.create(parentScope.declares)
+ )
+}
+
const importExportRE = /^Import|^Export/
function recordTypes(
if (overwriteId || node.id) types[overwriteId || getId(node.id!)] = node
break
case 'TSTypeAliasDeclaration':
- types[node.id.name] = node.typeAnnotation
+ types[node.id.name] = node.typeParameters ? node : node.typeAnnotation
break
case 'TSDeclareFunction':
if (node.id) declares[node.id.name] = node