Reputation: 21104
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?
Upvotes: 0
Views: 521
Reputation: 249466
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
Upvotes: 3