diff --git a/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md b/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md new file mode 100644 index 0000000000..fec0ac71fb --- /dev/null +++ b/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md @@ -0,0 +1,161 @@ +# Data Table Column Extensions for ASP.NET Core UI + +## Introduction + +Data table column extension system allows you to add a **new table column** on the user interface. The example below adds a new column with the "Social security no" title: + +![user-action-extension-click-me](../../images/table-column-extension-example.png) + +You can use the standard column options to fine control the table column. + +> Note that this is a low level API to find control the table column. If you want to show an extension property on the table, see the [module entity extension](../../Module-Entity-Extensions.md) document. + +## How to Set Up + +### Create a JavaScript File + +First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: + +![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) + +Here, the content of this JavaScript file: + +```js +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + columnList.addTail({ //add as the last column + title: 'Social security no', + data: 'extraProperties.SocialSecurityNumber', + orderable: false, + render: function (data, type, row) { + if (row.extraProperties.SocialSecurityNumber) { + return '' + + row.extraProperties.SocialSecurityNumber + + ''; + } else { + return 'undefined'; + } + } + }); + }); +``` + +This example defines a custom `render` function to return a custom HTML to render in the column. + +### Add the File to the User Management Page + +Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification). + +Write the following code inside the `ConfigureServices` of your module class: + +```csharp +Configure(options => +{ + options.ScriptBundles.Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, + bundleConfiguration => + { + bundleConfiguration.AddFiles( + "/Pages/Identity/Users/my-user-extensions.js" + ); + }); +}); +``` + +This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. + +### Rendering the Column + +This example assumes that you've defined a `SocialSecurityNumber` extra property using the [module entity extension](../../Module-Entity-Extensions.md) system. However; + +* You can add a new column that is related to an existing property of the user (that was not added to the table by default). Example: + +````js +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + columnList.addTail({ + title: 'Phone confirmed?', + data: 'phoneNumberConfirmed', + render: function (data, type, row) { + if (row.phoneNumberConfirmed) { + return 'YES'; + } else { + return 'NO'; + } + } + }); + }); +```` + +* You can add a new custom column that is not related to any entity property, but a completely custom information. Example: + +````js +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + columnList.addTail({ + title: 'Custom column', + data: {}, + orderable: false, + render: function (data) { + if (data.phoneNumber) { + return "call: " + data.phoneNumber; + } else { + return ''; + } + } + }); + }); +```` + +## API + +This section explains details of the `abp.ui.extensions.tableColumns` JavaScript API. + +### abp.ui.extensions.tableColumns.get(entityName) + +This method is used to access the table columns for an entity of a specific module. It takes one parameter: + +* **entityName**: The name of the entity defined by the related module. + +### abp.ui.extensions.tableColumns.get(entityName).columns + +The `columns` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined columns for a table. All contributors are executed in order to prepare the final column list. This is normally called by the modules to show the columns in the table. However, you can use it if you are building your own extensible UIs. + +### abp.ui.extensions.tableColumns.get(entityName).addContributor(contributeCallback [, order]) + +The `addContributor` method covers all scenarios, e.g. you want to add your column in a different position in the list, change or remove an existing column. `addContributor` has the following parameters: + +* **contributeCallback**: A callback function that is called whenever the column list should be created. You can freely modify the column list inside this callback method. +* **order** (optional): The order of the callback in the callback list. Your callback is added to the end of the list (so, you have opportunity to modify columns added by the previous contributors). You can set it `0` to add your contributor as the first item. + +#### Example + +```js +var myColumnDefinition = { + title: 'Custom column', + data: {}, + orderable: false, + render: function(data) { + if (data.phoneNumber) { + return "call: " + data.phoneNumber; + } else { + return ''; + } + } +}; + +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + // Remove an item from actionList + columnList.dropHead(); + + // Add a new item to the actionList + columnList.addHead(myColumnDefinition); + }); +``` + +> `columnList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. diff --git a/docs/en/UI/AspNetCore/Entity-Action-Extensions.md b/docs/en/UI/AspNetCore/Entity-Action-Extensions.md new file mode 100644 index 0000000000..f6e865375d --- /dev/null +++ b/docs/en/UI/AspNetCore/Entity-Action-Extensions.md @@ -0,0 +1,108 @@ +# Entity Action Extensions for ASP.NET Core UI + +## Introduction + +Entity action extension system allows you to add a **new action** to the action menu for an entity. A **Click Me** action was added to the *User Management* page below: + +![user-action-extension-click-me](../../images/user-action-extension-click-me.png) + +You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can access to the current entity in your code. + +## How to Set Up + +In this example, we will add a "Click Me!" action and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md). + +### Create a JavaScript File + +First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: + +![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) + +Here, the content of this JavaScript file: + +```js +var clickMeAction = { + text: 'Click Me!', + action: function(data) { + //TODO: Write your custom code + alert(data.record.userName); + } +}; + +abp.ui.extensions.entityActions + .get('identity.user') + .addContributor(function(actionList) { + actionList.addTail(clickMeAction); + }); +``` + +In the `action` function, you can do anything you need. See the API section for a detailed usage. + +### Add the File to the User Management Page + +Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification System](Bundling-Minification.md). + +Write the following code inside the `ConfigureServices` of your module class: + +```csharp +Configure(options => +{ + options.ScriptBundles.Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, + bundleConfiguration => + { + bundleConfiguration.AddFiles( + "/Pages/Identity/Users/my-user-extensions.js" + ); + }); +}); +``` + +This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. + +That's all. Run your application to see the result. + +## API + +This section explains details of the `abp.ui.extensions.entityActions` JavaScript API. + +### abp.ui.extensions.entityActions.get(entityName) + +This method is used to access the entity actions of a specific module. It takes one parameter: + +* **entityName**: The name of the entity defined by the related module. + +### abp.ui.extensions.entityActions.get(entityName).actions + +The `actions` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined actions for an entity. All contributors are executed in order to prepare the final actions list. This is normally called by the modules to show the actions in the grid. However, you can use it if you are building your own extensible UIs. + +### abp.ui.extensions.entityActions.get(entityName).addContributor(contributeCallback) + +The `addContributor` method covers all scenarios, e.g. you want to add your action in a different position in the list, change or remove an existing action item. `addContributor` with the following parameter: + +* **contributeCallback**: A callback function that is called whenever the action list should be created. You can freely modify the action list inside this callback method. + +#### Example + +```js +var clickMe2Action = { + text: 'Click Me 2!', + icon: 'fas fa-hand-point-right', + action: function(data) { + //TODO: Write your custom code + alert(data.record.userName); + } +}; + +abp.ui.extensions.entityActions + .get('identity.user') + .addContributor(function(actionList) { + // Remove an item from actionList + actionList.dropHead(); + + // Add the new item to the actionList + actionList.addHead(clickMe2Action); + }); +``` + +> `actionList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. diff --git a/docs/en/images/table-column-extension-example.png b/docs/en/images/table-column-extension-example.png new file mode 100644 index 0000000000..84d87bf46c Binary files /dev/null and b/docs/en/images/table-column-extension-example.png differ diff --git a/docs/en/images/user-action-extension-click-me.png b/docs/en/images/user-action-extension-click-me.png new file mode 100644 index 0000000000..4046630f48 Binary files /dev/null and b/docs/en/images/user-action-extension-click-me.png differ diff --git a/docs/en/images/user-action-extension-on-solution.png b/docs/en/images/user-action-extension-on-solution.png new file mode 100644 index 0000000000..5f58d3f302 Binary files /dev/null and b/docs/en/images/user-action-extension-on-solution.png differ diff --git a/npm/ng-packs/packages/schematics/src/constants/volo.ts b/npm/ng-packs/packages/schematics/src/constants/volo.ts index 12aa166d66..00fde72c79 100644 --- a/npm/ng-packs/packages/schematics/src/constants/volo.ts +++ b/npm/ng-packs/packages/schematics/src/constants/volo.ts @@ -1,22 +1 @@ -import { Interface, Property } from '../models'; - export const VOLO_REGEX = /^Volo\.Abp\.(Application\.Dtos|ObjectExtending)/; - -export const VOLO_NAME_VALUE = new Interface({ - base: null, - identifier: 'NameValue', - ref: 'Volo.Abp.NameValue', - namespace: 'Volo.Abp', - properties: [ - new Property({ - name: 'name', - type: 'string', - refs: ['System.String'], - }), - new Property({ - name: 'value', - type: 'T', - refs: ['T'], - }), - ], -}); diff --git a/npm/ng-packs/packages/schematics/src/models/model.ts b/npm/ng-packs/packages/schematics/src/models/model.ts index 6c7feb318d..048c08a1b0 100644 --- a/npm/ng-packs/packages/schematics/src/models/model.ts +++ b/npm/ng-packs/packages/schematics/src/models/model.ts @@ -1,43 +1,97 @@ import { Import } from './import'; -import { Omissible } from './util'; +import { Options } from './util'; export class Model { - imports: Import[] = []; - interfaces: Interface[] = []; - namespace: string; - path: string; + readonly imports: Import[] = []; + readonly interfaces: Interface[] = []; + readonly namespace: string; + readonly path: string; constructor(options: ModelOptions) { Object.assign(this, options); } } -export type ModelOptions = Omissible; +export type ModelOptions = Options; export class Interface { - base: string | null; - identifier: string; - namespace: string; - properties: Property[] = []; - ref: string; + readonly base: string | null; + readonly identifier: string; + readonly namespace: string; + readonly generics: Generic[] = []; + readonly properties: Property[] = []; + readonly ref: string; constructor(options: InterfaceOptions) { Object.assign(this, options); } } -export type InterfaceOptions = Omissible; +export type InterfaceOptions = Options; -export class Property { - name: string; - type: string; - default = ''; - optional: '' | '?' = ''; - refs: string[] = []; +abstract class TypeRef { + readonly refs: string[] = []; - constructor(options: PropertyOptions) { + protected _type = ''; + get type() { + return this._type; + } + set type(value: string) { + if (!value) return; + this._type = value; + } + + protected _default = ''; + get default() { + return this._default; + } + set default(value: string) { + if (!value) return; + this._default = ` = ${value}`; + } + + constructor(options: TypeRefOptions) { Object.assign(this, options); } + + setDefault(value: string) { + this.default = value; + } + + setType(value: string) { + this.type = value; + } +} + +type TypeRefOptionalKeys = 'default' | 'refs'; +type TypeRefOptions = Options; + +export class Generic extends TypeRef { + constructor(options: GenericOptions) { + super(options); + } +} + +export type GenericOptions = Options; + +export class Property extends TypeRef { + readonly name: string; + private _optional: '' | '?' = ''; + get optional() { + return this.default ? '' : this._optional; + } + + set optional(value: '' | '?') { + this._optional = value; + } + + constructor(options: PropertyOptions) { + super(options); + } + + setOptional(isOptional: boolean) { + this.optional = isOptional ? '?' : ''; + } } -export type PropertyOptions = Omissible; +export type PropertyOptions = Options; diff --git a/npm/ng-packs/packages/schematics/src/models/util.ts b/npm/ng-packs/packages/schematics/src/models/util.ts index 5ff732ccfe..0588ed7d5b 100644 --- a/npm/ng-packs/packages/schematics/src/models/util.ts +++ b/npm/ng-packs/packages/schematics/src/models/util.ts @@ -1,2 +1,16 @@ // Omissible (given keys will become optional) export type Omissible = Partial> & Omit; + +// ExcludeKeys (keys will be excluded based on their type) +type ExcludeKeys = Exclude< + { + [Key in keyof Type]: Type[Key] extends Excluded ? never : Key; + }[keyof Type], + never +>; + +// tslint:disable-next-line: ban-types +type ExcludeMethods = Pick>; + +// Options (methods will be omitted, given keys will become optional) +export type Options> = Omissible, K>; diff --git a/npm/ng-packs/packages/schematics/src/utils/generics.ts b/npm/ng-packs/packages/schematics/src/utils/generics.ts new file mode 100644 index 0000000000..8dc7fb2629 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/generics.ts @@ -0,0 +1,100 @@ +import { Generic } from '../models'; + +export class GenericsCollector { + private _generics: Generic[] = []; + get generics() { + return this._generics; + } + + apply = (value: string, index: number) => { + const generic = this.get(index); + if (generic) { + if (!generic.type) generic.setType(value); + return value + generic.default; + } + + return value; + }; + + constructor(private getTypeIdentifier = (type: string) => type) {} + + private createGeneric(type: string, ref: string, defaultValue: string) { + const _default = this.getTypeIdentifier(defaultValue); + const refs = [generateRefWithPlaceholders(ref)]; + const generic = new Generic({ type, default: _default, refs }); + return generic; + } + + private register(index: number, generic: Generic) { + const existing = this.get(index); + if (existing) { + existing.setDefault(generic.default); + existing.setType(generic.type); + } else this.set(index, generic); + } + + collect(generics: string[], genericArguments: string[]) { + generics.forEach((ref, i) => { + const generic = this.createGeneric( + genericArguments[i], + ref, + genericArguments.includes(ref) ? '' : ref, + ); + this.register(i, generic); + }); + } + + get(index: number) { + return this.generics[index]; + } + + set(index: number, value: Generic) { + this.generics[index] = value; + } + + reset() { + this._generics = []; + } +} + +export function generateRefWithPlaceholders(sourceType: string) { + let { identifier, generics } = extractGenerics(sourceType); + + identifier = identifier; + generics = generics.map((_, i) => `T${i}`); + + return generics.length ? `${identifier}<${generics}>` : identifier; +} + +export function extractSimpleGenerics(sourceType: string) { + const { identifier, generics } = extractGenerics(sourceType); + + return { + identifier: getLastSegment(identifier), + generics: generics.map(getLastSegment), + }; +} + +export function extractGenerics(sourceType: string) { + const regex = /(?[^<]+)(<(?.+)>)?/g; + const { identifier = '', generics = '' } = regex.exec(sourceType)?.groups ?? {}; + + return { + identifier, + generics: generics.split(/,\s*/).filter(Boolean), + }; +} + +function getLastSegment(str: string) { + return str.split('.').pop()!; +} + +export function replacePlaceholdersWithGenerics( + type: string, + generics: string[], + genericsCollector: GenericsCollector, +) { + return generics + .map(genericsCollector.apply) + .reduce((acc, v, i) => acc.replace(new RegExp(`([<, ])T${i}([,>])`, 'g'), `$1${v}$2`), type); +} diff --git a/npm/ng-packs/packages/schematics/src/utils/index.ts b/npm/ng-packs/packages/schematics/src/utils/index.ts index 178920dccc..e6df05b2e2 100644 --- a/npm/ng-packs/packages/schematics/src/utils/index.ts +++ b/npm/ng-packs/packages/schematics/src/utils/index.ts @@ -5,6 +5,7 @@ export * from './barrel'; export * from './common'; export * from './enum'; export * from './file'; +export * from './generics'; export * from './import'; export * from './model'; export * from './namespace'; diff --git a/npm/ng-packs/packages/schematics/src/utils/model.ts b/npm/ng-packs/packages/schematics/src/utils/model.ts index fc8dde37c1..49d3c0fcf5 100644 --- a/npm/ng-packs/packages/schematics/src/utils/model.ts +++ b/npm/ng-packs/packages/schematics/src/utils/model.ts @@ -1,5 +1,11 @@ -import { VOLO_NAME_VALUE, VOLO_REGEX } from '../constants'; +import { VOLO_REGEX } from '../constants'; import { Interface, Model, Property, PropertyDef, Type, TypeWithEnum } from '../models'; +import { + extractGenerics, + generateRefWithPlaceholders, + GenericsCollector, + replacePlaceholdersWithGenerics, +} from './generics'; import { parseNamespace } from './namespace'; import { relativePathToModel } from './path'; import { camel } from './text'; @@ -8,6 +14,7 @@ import { createTypeParser, createTypeSimplifier, createTypesToImportsReducer, + extendsSelf, removeTypeModifiers, } from './type'; @@ -27,6 +34,7 @@ export function createImportRefsToModelReducer(params: ModelGeneratorParams) { return (models: Model[], importRefs: string[]) => { const enums: string[] = []; const interfaces = importRefs.reduce(reduceImportRefsToInterfaces, []); + sortInterfaces(interfaces); interfaces.forEach(_interface => { @@ -40,12 +48,9 @@ export function createImportRefsToModelReducer(params: ModelGeneratorParams) { const index = models.findIndex(m => m.namespace === _interface.namespace); if (index > -1) { if (models[index].interfaces.some(i => i.identifier === _interface.identifier)) return; - if (_interface.ref.startsWith(VOLO_NAME_VALUE.ref)) return; models[index].interfaces.push(_interface); } else { - if (_interface.ref.startsWith(VOLO_NAME_VALUE.ref)) _interface = VOLO_NAME_VALUE; - const { namespace } = _interface; models.push( @@ -70,7 +75,7 @@ export function createImportRefsToModelReducer(params: ModelGeneratorParams) { isEnum: false, }); - _interface.properties.forEach(prop => { + [..._interface.properties, ..._interface.generics].forEach(prop => { prop.refs.forEach(ref => { const propType = types[ref]; if (!propType) return; @@ -100,38 +105,50 @@ export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorP const parseType = createTypeParser(removeTypeModifiers); const simplifyType = createTypeSimplifier(); const getIdentifier = (type: string) => removeTypeModifiers(simplifyType(type)); + const genericsCollector = new GenericsCollector(getIdentifier); return reduceRefsToInterfaces; - function reduceRefsToInterfaces(interfaces: Interface[], ref: string) { + function reduceRefsToInterfaces(interfaces: Interface[], ref: string): Interface[] { const typeDef = types[ref]; if (!typeDef) return interfaces; const namespace = parseNamespace(solution, ref); - const identifier = (typeDef.genericArguments ?? []).reduce( - (acc, t, i) => acc.replace(`T${i}`, t), - getIdentifier(ref), - ); + let { baseType: base, genericArguments } = typeDef; + genericArguments = genericArguments || []; + let identifier = getIdentifier(ref); + identifier = replacePlaceholdersWithGenerics(identifier, genericArguments, genericsCollector); + + if (base) { + if (extendsSelf(ref, base)) { + genericsCollector.collect(extractGenerics(base).generics, genericArguments); + return reduceRefsToInterfaces(interfaces, generateRefWithPlaceholders(base)); + } else { + base = getIdentifier(base); + } + } - const base = typeDef.baseType ? getIdentifier(typeDef.baseType) : null; - const _interface = new Interface({ identifier, base, namespace, ref }); + const { generics } = genericsCollector; + const _interface = new Interface({ identifier, base, namespace, ref, generics }); + genericsCollector.reset(); typeDef.properties?.forEach(prop => { const name = camel(prop.name); - const optional = isOptionalProperty(prop) ? '?' : ''; const type = simplifyType(prop.typeSimple); const refs = parseType(prop.type).reduce( (acc: string[], r) => acc.concat(parseGenerics(r).toGenerics()), [], ); + const property = new Property({ name, type, refs }); + property.setOptional(isOptionalProperty(prop)); - _interface.properties.push(new Property({ name, optional, type, refs })); + _interface.properties.push(property); }); interfaces.push(_interface); - return _interface.properties + return [..._interface.properties, ..._interface.generics] .reduce((refs, prop) => { prop.refs.forEach(type => { if (types[type]?.isEnum) return; @@ -142,7 +159,7 @@ export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorP return refs; }, []) .concat(base ? parseGenerics(typeDef.baseType!).toGenerics() : []) - .reduce(reduceRefsToInterfaces, interfaces); + .reduce(reduceRefsToInterfaces, interfaces); } } diff --git a/npm/ng-packs/packages/schematics/src/utils/service.ts b/npm/ng-packs/packages/schematics/src/utils/service.ts index b8a2d692b2..70655c821d 100644 --- a/npm/ng-packs/packages/schematics/src/utils/service.ts +++ b/npm/ng-packs/packages/schematics/src/utils/service.ts @@ -83,8 +83,8 @@ export function createActionToSignatureMapper() { signature.parameters = action.parametersOnMethod.map(p => { const type = adaptType(p.typeSimple); const parameter = new Property({ name: p.name, type }); - if (p.defaultValue) parameter.default = ` = ${p.defaultValue}`; - else if (p.isOptional) parameter.optional = '?'; + parameter.setDefault(p.defaultValue); + parameter.setOptional(p.isOptional); return parameter; }); diff --git a/npm/ng-packs/packages/schematics/src/utils/type.ts b/npm/ng-packs/packages/schematics/src/utils/type.ts index 781794c463..a087392d40 100644 --- a/npm/ng-packs/packages/schematics/src/utils/type.ts +++ b/npm/ng-packs/packages/schematics/src/utils/type.ts @@ -1,7 +1,7 @@ -import { SYSTEM_TYPES } from '../constants'; -import { VOLO_REGEX } from '../constants/volo'; +import { SYSTEM_TYPES, VOLO_REGEX } from '../constants'; import { eImportKeyword } from '../enums'; import { Import, TypeWithEnum } from '../models'; +import { extractSimpleGenerics } from './generics'; import { parseNamespace } from './namespace'; import { relativePathToEnum, relativePathToModel } from './path'; import { parseGenerics } from './tree'; @@ -15,14 +15,9 @@ export function createTypeSimplifier() { type = /any.+)<.*(?<=[\.<])(?.+)>/gm); - const { generic, genericType } = regexp.exec(type)?.groups ?? {}; + const { identifier, generics } = extractSimpleGenerics(type); - return generic - ? generic === 'any' - ? 'any' - : `${generic}<${genericType}>` - : type.split('.').pop()!; + return generics.length ? `${identifier}<${generics.join(', ')}>` : identifier; }); return (type: string) => { @@ -40,9 +35,7 @@ export function createTypeParser(replacerFn = (t: string) => t) { export function createTypeNormalizer(replacerFn = (t: string) => t) { return (type: string) => { - type = normalizeTypeAnnotations(type); - - return replacerFn(type); + return replacerFn(normalizeTypeAnnotations(type)); }; } @@ -56,8 +49,7 @@ export function flattenDictionaryTypes(types: string[], type: string) { } export function normalizeTypeAnnotations(type: string) { - type = type.replace(/\[(.+)+\]/g, '$1[]'); - return type.replace(/\?/g, ''); + return type.replace(/\[(.+)+\]/g, '$1[]').replace(/\?/g, ''); } export function removeGenerics(type: string) { @@ -115,3 +107,8 @@ export function createTypeAdapter() { const simplifyType = createTypeSimplifier(); return (type: string) => parseGenerics(type, node => simplifyType(node.data)).toString(); } + +// naming here is depictive only +export function extendsSelf(type: string, base: string) { + return removeGenerics(base) === removeGenerics(type); +}