简体   繁体   中英

TypeScript complex generic method

I currently have this method

create<T extends ElementType | string>(type: T): Element<T>;

which uses

export type ElementType = 'ExtensionElements' | 'Documentation';

export type Element<T> =
  T extends 'ExtensionElements' ? ExtensionElements :
    T extends 'Documentation' ? Documentation :
      GenericElement;

This method is in a .d.ts , and it guarantees the result is always typed, so that

const e1 = obj.create('ExtensionElements');
      ^^ type is ExtensionElements

const e2 = obj.create('Documentation');
      ^^ type is Documentation

const e3 = obj.create('Other');
      ^^ type is GenericElement

Now, I'd like to let the users of this method extend the possible typed choices, so that, for example

type CustomElementType = 'Other' | ElementType;

type CustomElement<T> =
  T extends 'Other' ? CustomOtherElement : Element<T>;

const e4 = obj.create<CustomElementType, CustomElement>('Other');
      ^^ type is CustomOtherElement

However this doesn't seem to work correctly, as I always receive an union of all types, and I cannot use arbitrary strings.

Do you have any other idea how I could implement this?

You can use an interface to map from the string type to the true type. Since interfaces are open ended clients can use module augmentation to add extra options:

// create.ts
export declare let obj: {   
    create<T extends ElementType | string>(type: T): Element<T>;
}
type ExtensionElements = { e: string }
type Documentation = { d: string }
type GenericElement = { g: string }

export type ElementType = 'ExtensionElements' | 'Documentation';
export interface ElementMap {
    'ExtensionElements': ExtensionElements;
    'Documentation': Documentation;
}
export type Element<T extends string> = ElementMap extends Record<T, infer E> ? E :
    GenericElement;

const e1 = obj.create('ExtensionElements'); // ExtensionElements
const e2 = obj.create('Documentation'); // Documentation
const e3 = obj.create('Else'); //GenericElement

// create-usage.ts
import { obj } from './create'
type CustomOtherElement = { x: string }

declare module './create' {
    export interface ElementMap {
        'Other': CustomOtherElement
    }
}
const e4 = obj.create('Other'); //  CustomOtherElement

If you want to do scoped extension you will need an extra function that will change the interface used to map the string to the object type. This method can just return the current object cast as the expected result (since types don't matter at runtime nothing needs to be different)

// create.ts
interface Creator<TMap = ElementMap>{
    create<T extends keyof TMap | string>(type: T): Element<TMap, T>;
    extend<TMapExt extends TMap>(): Creator<TMapExt>
}
export declare let obj: Creator
type ExtensionElements = { e: string }
type Documentation = { d: string }
type GenericElement = { g: string }

import { obj, ElementMap } from './create'
type CustomOtherElement = { x: string }

export type ElementType = 'ExtensionElements' | 'Documentation';
export interface ElementMap {
    'ExtensionElements': ExtensionElements;
    'Documentation': Documentation;
}
export type Element<TMap, T extends PropertyKey> = TMap extends Record<T, infer E> ? E : GenericElement;

const e1 = obj.create('ExtensionElements'); // ExtensionElements
const e2 = obj.create('Documentation'); // Documentation
const e3 = obj.create('Else'); //GenericElement


// create-usage.ts
export interface CustomElementMap extends ElementMap {
    'Other': CustomOtherElement
}
const customObj = obj.extend<CustomElementMap>()
const e4 = customObj.create('Other'); //  CustomOtherElement

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM