Preserving generics in TypeScript mapped types - javascript

I have a class, whose instance methods are handlers, each of which represents an operation, taking a reference as the input and assign the output to the second parameter. A proxy object is generated by a third party library so that the handlers can be called directly.
type InputRef<T> = {
current: T,
};
type OutputRef<T> = {
current?: T,
};
class Original {
increment(input: InputRef<number>, output: OutputRef<number>) {
const { current: inValue } = input;
output.current = inValue + 1;
}
}
type Mapper<Fn> = Fn extends (input: InputRef<infer U>, output: OutputRef<infer V>) => unknown ? (input: U) => V : never;
type MyProxyGeneratedByThirdPartyJs = { [FnName in keyof Original]: Mapper<Original[FnName]> };
declare const proxy: MyProxyGeneratedByThirdPartyJs;
const result = proxy.increment(3); // 4
However, the mapper does not work when the handler involves generic types, e.g.,
class Original {
toBox<T>(input: InputRef<T>, output: OutputRef<{ boxed: T }>) {
const { current: inValue } = input;
output.current = { boxed: inValue };
}
}
Using the same way above, the type of proxy only involves unknown, and the generic information T is lost.
Ideally, I want proxy to be of type
{
toBox<T>(input: T): { boxed: T },
}
instead of
{
toBox(input: unknown): { boxed: unknown },
}
Is there any way achieving this?

This is not currently possible in TypeScript. In order to express what you're doing to generic functions at the type level, you'd need higher kinded types as requested in microsoft/TypeScript#1213, but these are not directly supported. The conditional type definition of Mapper<Fn> infers U as the input type and V as the output type, but there is no capacity for the compiler to see or represent any higher-order relationship between them when Fn is generic.
There is some support for transforming generic function types into other generic function types, but this only happens in very specific circumstances. The transformation needs to happen at least partially at the value level (there must be a function value which transforms one generic function type into another when called, not just the type of that function) so there will be JS code emitted. And the transformation only works for a single function at a time, so an object of functions cannot be mapped at once without losing the generics.
Still, to show that there is some ability to do this, here is how one might approach it:
function proxySingleFunction<U, V>(
f: (input: InputRef<U>, output: OutputRef<V>) => any
): (input: U) => V {
return function (input: U) {
const o: OutputRef<V> = {};
f({ current: input }, o);
const ret = o.current;
if (ret === undefined) throw new Error("OH NO");
return ret;
}
}
The type of proxySingleFunction() is
// function proxySingleFunction<U, V>(
// f: (input: InputRef<U>, output: OutputRef<V>) => any
// ): (input: U) => V
which looks similar to what you're doing with Mapper<Fn>. Then, if you call proxySingleFunction(), it will produce outputs of the relevant type:
const increment = proxySingleFunction(Original.prototype.increment);
// const increment: (input: number) => number
const toBox = proxySingleFunction(Original.prototype.toBox);
// const toBox: <T>(input: T) => { boxed: T; }
You can see that toBox is generic, as desired. Then you could pacakge these output functions in a single proxy object and use it:
const proxy = {
increment, toBox
}
console.log(proxy.increment(1).toFixed(1)) // "2.0"
console.log(proxy.toBox("a").boxed.toUpperCase()) // "A"
So that's great and it works. But it requires that you emit JavaScript for each method you want to transform. If you already have such a transformed object from a third party and just want to represent the typings, the closest you can get is to lie to the compiler via type assertions so that it thinks you're doing the transformations when you're actually not:
// pretend this function exists
declare const psf: <U, V>(
f: (input: InputRef<U>, output: OutputRef<V>) => any
) => (input: U) => V;
// pretend you're using it to create a proxy object
const myProxyType = (true as false) || {
increment: psf(Original.prototype.increment),
toBox: psf(Original.prototype.toBox)
}
// get the pretend type of that object
type MyProxyGeneratedByThirdPartyJs = typeof myProxyType;
/* type MyProxyGeneratedByThirdPartyJs = {
increment: (input: number) => number;
toBox: <T>(input: T) => { boxed: T; };
} */
I don't see this as a big win over just writing out these types manually in the first place, so I don't know that I'd recommend it. It's just the closest I can imagine getting to what you want with the language as it currently is.
Playground link to code

Related

Preserve functions typing when augmenting an object in Typescript

I need to perform some transformations on a typescript object while being able to preserve it's full shape.
So, I have an object:
const simpleObject = {
one: () => ({}),
two: (a: number) => ({ a }),
};
type SimpleObject<K, V> = {
[P in keyof K]: V; // Not sure how to type V correctly to be a function that takes any arguments and outputs an object
};
where the values are generic functions that output an object.
I need to perform some transformations on the object, so that:
type AugmentedObject<K, V> = {
[P in keyof K]: V; // again, typing of V is not correct here, it should be a generic function that outputs an object
};
function augmentObject<K, V>(simpleObject: SimpleObject<K, V>): AugmentedObject<K, V> {
// perform some transformation
// ...
return simpleObject;
}
augmentObject(simpleObject).one; // the type should resolve to a () => object
augmentObject(simpleObject).two; // the type should resolve to (a: number) => object
I cannot figure out how to maintain the correct typing for one or two as I can't figure out how to generically pass a function
I'not sure this is the solution you're looking for. Though the final types are just the exact shape you're expecting them to be.
const simpleObject = {
one: () => ({}),
two: (a: number) => ({ a }),
};
type SimpleObject
= { [K in string | number | symbol]: (...args: any[]) => object }
function augmentObject<T extends SimpleObject>(simpleObject: T): T {
// perform some transformation
// ...
return simpleObject;
}
const a = augmentObject(simpleObject)
type One = typeof a.one // type One = () => {}
type Two = typeof a.two // type Two = (a: number) => { a: number }
TS playground

TypeScript: Having difficulty typing a function that can filter on a specific level inside of an object while preserving its original structure

I have an object called myCached and its type is known as
interface MyCache {
a: {
b: {
c: Target[];
d: string;
};
};
}
Where Target is the type of the item in one of its property on a specific level, this type Target is known too. Assume Target is like this
type Target = "toRemove1" | "toRemove2" | "toPreserve1" | "toPreserve2";
In the example here, myCache is
const cache: MyCache = {
a: {
b: {
c: ['toRemove1', 'toPreserve1', 'toRemove2', 'toPreserve2'],
d: "irrelevant"
},
}
}
And I have an array of items that I need to remove. They also have the type of Target as in
const thingsToRemove: Target[] = ["toRemove1", "toRemove2"];
And I am trying to come up with a function that can traverse this object and filter out items on a specific level. The way I designed this function is that it takes a transformer object and it travers the object and it provides the function for filtering. I want to type it properly so the users of this function can reply on the auto complete provided by the TS compiler and type checking to make sure that this function lands correctly on the correct level, in this case it is the Target level.
This is my code
type MappedTransform<T> = {
[K in keyof T]?: MappedTransform<T[K]> | ((params: T[K]) => T[K]);
};
type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
function traverse<R>(cache: R, transformObject: MappedTransform<R>): R {
return (Object.entries(transformObject) as Array<
Entries<MappedTransform<R>>
>).reduce(reduceTransformNode, cache);
}
const reduceTransformNode = <R, K extends keyof R>(
cacheNode: R,
[transformKey, transformValue]: [
K,
MappedTransform<R[K]> | ((params: R[K]) => R[K]) | undefined
]
): R => {
const { [transformKey]: node } = cacheNode;
if (typeof transformValue === "undefined") return cacheNode;
const newCacheValue =
typeof transformValue === "function"
? (transformValue as (params: R[K]) => R[K])(node)
: traverse(transformValue as R[K], node);
return {
...cacheNode,
[transformKey]: newCacheValue
};
};
const x = traverse(cache, {
a: {
b: {
// 👇Need to use Type `Target` to make sure that the transformer function lands on the type of `Target` exactly
c: (node) => node.filter((s) => !thingsToRemove.includes(s))
}
}
});
This works fine and I have been really close to what I wanted to achieve except that I cannot seem to find a way to add the type of Target to make sure that the function the user provides to do the filtering actually is on the right level i.e. node is of the right type Target
Here is the live demo. Can someone tell me how to achieve that last bit of type safety I am seeking for here?

How to de-structure an enum values in typescript?

I have an enum in typescript like below:
export enum XMPPElementName {
state = "state",
presence = "presence",
iq = "iq",
unreadCount = "uc",
otherUserUnreadCount = "ouc",
sequenceID = "si",
lastSequenceID = "lsi",
timeStamp = "t",
body = "body",
message = "message"
}
And wants to de-structure its value, How can we do this in Typescript?
const { uc, ouc, msg, lsi, si, t, body } = XMPPElementName;
update
As #amadan mentioned, we can use Assigning to new variable names as in Mozilla doc say Destructuring_assignment, like below:
Assigning to new variable names
A property can be unpacked from an object and assigned to a variable with a different name than the object property.
const o = {p: 42, q: true};
const {p: foo, q: bar} = o;
console.log(foo); // 42
console.log(bar); // true
And the method is very good to solve this problem, but if you need to access all items without the need to explicitly define them, you can either on of these two mentiond tag1 tag2
const { uc, ouc, msg, lsi, si, t, body } = XMPPElementName;
This doesn't work because XMPPElementName doesn't have an element named uc (and equivalently for others). If you explicitly name your keys, it will work:
const {
unreadCount: uc,
otherUserUnreadCount: ouc,
message: msg,
lastSequenceID: lsi,
sequenceID: si,
timeStamp: t,
body: body,
} = XMPPElementName;
it will work. Alternately, you can just use variables with names that are equal to the keys, not the values:
const {
unreadCount,
otherUserUnreadCount,
message,
lastSequenceID,
sequenceID,
timeStamp,
body,
} = XMPPElementName;
You want an enum value-to-value map. Like you've said enum in JS is just a POJO. You can create a utility type to help generate the correct type.
type EnumValueMap<T extends { [k: string]: string }> = { [K in T[keyof T]]: K }
function convertEnumValuesToObject<T extends { [k: string]: string }>(enumerable: T): EnumValueMap<T> {
return (Object as any).fromEntries(Object.values(enumerable).map(v => [v, v]))
}
Playground Link
As we know, in typescript an enum is like a plain old javascript object(at-least what the playground js-output is showing or the log showing):
one way is using a function which generates a new object with {value:value} structure like below:
export function convertEnumValuesToObject<T>(enumObj: T): { [index: string]: T[keyof T] } {
const enum_values = Object.values(enumObj);
return Object.assign({}, ...enum_values.map(_ => ({ [_]: _ })));
}
const { uc, ouc, msg, lsi, si, t, body } = convertEnumValuesToObject(
XMPPElementName
);
It would be great to see answers in typescript?
This may be helpful for anyone looking for a quick and easy answer - yes you can (at least as of now). This works for enums with and without assigned values as far as I can tell.
enum MyEnum {
One,
Two,
Three
}
const { One, Two, Three } = myEnum;
console.log({ One, Two, Three }) // {One: 0, Two: 1, Three: 2}
enum Status {
None = '',
Created = 'CREATED',
Completed = 'COMPLETED',
Failed = 'FAILED',
}
const { None, Created, Completed, Failed } = Status;
console.log(None, Created, Completed, Failed) // '', 'CREATED', 'COMPLETED, 'FAILED'
Please write me back if I'm wrong or you found any weirdness when testing yourself.

How to implement Swift-style enum with associated values in Typescript? [duplicate]

Unfortunately, as of 0.9.5, TypeScript doesn't (yet) have algebraic data types (union types) and pattern matching (to destructure them). What's more, it doesn't even support instanceof on interfaces. Which pattern do you use to emulate these language features with maximal type safety and minimal boilerplate code?
I went with the following Visitor-like pattern, inspired by this and this (in the example, a Choice can be Foo or Bar):
interface Choice {
match<T>(cases: ChoiceCases<T>): T;
}
interface ChoiceCases<T> {
foo(foo: Foo): T;
bar(bar: Bar): T;
}
class Foo implements Choice {
match<T>(cases: ChoiceCases<T>): T {
return cases.foo(this);
}
}
class Bar implements Choice {
match<T>(cases: ChoiceCases<T>): T {
return cases.bar(this);
}
}
Usage:
function getName(choice: Choice): string {
return choice.match({
foo: foo => "Foo",
bar: bar => "Bar",
});
}
The matching itself is expressive and type-safe, but there's lot of boilerplate to write for the types.
Example to illustrate the accepted answer:
enum ActionType { AddItem, RemoveItem, UpdateItem }
type Action =
{type: ActionType.AddItem, content: string} |
{type: ActionType.RemoveItem, index: number} |
{type: ActionType.UpdateItem, index: number, content: string}
function dispatch(action: Action) {
switch(action.type) {
case ActionType.AddItem:
// now TypeScript knows that "action" has only "content" but not "index"
console.log(action.content);
break;
case ActionType.RemoveItem:
// now TypeScript knows that "action" has only "index" but not "content"
console.log(action.index);
break;
default:
}
}
TypeScript 1.4 adds union types and type guards.
Here's an alternative to the very good answer by #thSoft. On the plus side, this alternative
has potential interoperability with raw javascript objects on the form { type : string } & T, where the shape of T depends on the value of type,
has substantially less per-choice boilerplate;
on the negative side
does not enforce statically that you match all cases,
does not distinguish between different ADTs.
It looks like this:
// One-time boilerplate, used by all cases.
interface Maybe<T> { value : T }
interface Matcher<T> { (union : Union) : Maybe<T> }
interface Union { type : string }
class Case<T> {
name : string;
constructor(name: string) {
this.name = name;
}
_ = (data: T) => ( <Union>({ type : this.name, data : data }) )
$ =
<U>(f:(t:T) => U) => (union : Union) =>
union.type === this.name
? { value : f((<any>union).data) }
: null
}
function match<T>(union : Union, destructors : Matcher<T> [], t : T = null)
{
for (const destructor of destructors) {
const option = destructor(union);
if (option)
return option.value;
}
return t;
}
function any<T>(f:() => T) : Matcher<T> {
return x => ({ value : f() });
}
// Usage. Define cases.
const A = new Case<number>("A");
const B = new Case<string>("B");
// Construct values.
const a = A._(0);
const b = B._("foo");
// Destruct values.
function f(union : Union) {
match(union, [
A.$(x => console.log(`A : ${x}`))
, B.$(y => console.log(`B : ${y}`))
, any (() => console.log(`default case`))
])
}
f(a);
f(b);
f(<any>{});
To answer
it doesn't even support instanceof on interfaces.
Reason is type erasure. Interfaces are a compile type construct only and don't have any runtime implications. However you can use instanceof on classes e.g. :
class Foo{}
var x = new Foo();
console.log(x instanceof Foo); // true
This is an old question, but maybe this will still help someone:
Like #SorenDebois's answer, this one has half of the per-case boilerplate as #theSoft's. It is also more encapsulated than #Soren's. Additionally, this solution has type safety, switch-like behavior, and forces you to check all cases.
// If you want to be able to not check all cases, you can wrap this type in `Partial<...>`
type MapToFuncs<T> = { [K in keyof T]: (v: T[K]) => void }
// This is used to extract the enum value type associated with an enum.
type ValueOfEnum<_T extends Enum<U>, U = any> = EnumValue<U>
class EnumValue<T> {
constructor(
private readonly type: keyof T,
private readonly value?: T[keyof T]
) {}
switch(then: MapToFuncs<T>) {
const f = then[this.type] as (v: T[keyof T]) => void
f(this.value)
}
}
// tslint:disable-next-line: max-classes-per-file
class Enum<T> {
case<K extends keyof T>(k: K, v: T[K]) {
return new EnumValue(k, v)
}
}
Usage:
// Define the enum. We only need to mention the cases once!
const GameState = new Enum<{
NotStarted: {}
InProgress: { round: number }
Ended: {}
}>()
// Some function that checks the game state:
const doSomethingWithState = (state: ValueOfEnum<typeof GameState>) => {
state.switch({
Ended: () => { /* One thing */ },
InProgress: ({ round }) => { /* Two thing with round */ },
NotStarted: () => { /* Three thing */ },
})
}
// Calling the function
doSomethingWithState(GameState.case("Ended", {}))
The one aspect here that is really not ideal is the need for ValueOfEnum. In my application, that was enough for me to go with #theSoft's answer. If anyone knows how to compress this, drop a comment below!

Map Typescript Enum

How would I map a typescript enum? For example, with strings you can do this:
let arr = [ 'Hello', 'Goodbye' ];
arr.map(v => {
if (v === 'Hello') {
return ':)';
} else if (v === 'Goodbye') {
return ':(';
}
); // [ ':)', ':(' ]
This, of course, doesn't work with enums:
enum MyEnum { Hello, Goodbye };
MyEnum.map(v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // does not work
Ideally, I'd like to do this in a generalized way so I can simply take any enum I have and put it through a map function while preserving type information. Usage might look something like this:
map(MyEnum, v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // [ ':)', ':(' ]
I've been fiddling around with getting a function that does this for me but keep having issues getting the generics just right.
To map an enum do this:
(Object.keys(MyEnum) as Array<keyof typeof MyEnum>).map((key) => {})
The function to solve this is quite simple.
// you can't use "enum" as a type, so use this.
type EnumType = { [s: number]: string };
function mapEnum (enumerable: EnumType, fn: Function): any[] {
// get all the members of the enum
let enumMembers: any[] = Object.keys(enumerable).map(key => enumerable[key]);
// we are only interested in the numeric identifiers as these represent the values
let enumValues: number[] = enumMembers.filter(v => typeof v === "number");
// now map through the enum values
return enumValues.map(m => fn(m));
}
As you can see, we first need to get all of the keys for the enum (MyEnum.Hello is actually 1 at runtime) and then just map through those, passing the function on.
Using it is also simple (identical to your example, although I changed the name):
enum MyEnum { Hello, Goodbye };
let results = mapEnum(MyEnum, v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
});
console.log(results); // [ ':)', ':(' ]
The reason we need to filter the enum to be numbers only is because of the way enums are compiled.
Your enum is actually compiled to this:
var MyEnum;
(function (MyEnum) {
MyEnum[MyEnum["Hello"] = 0] = "Hello";
MyEnum[MyEnum["Goodbye"] = 1] = "Goodbye";
})(MyEnum || (MyEnum = {}));
;
However we are not interested in "Hello" or "Goodbye" as we can't use those at runtime.
You will also notice a funny type statement right before the function. This is because you can't type a parameter as someParameter: enum, you need to explicitly state it as a number -> string map.
Mapping in Typescript can be extremely powerful for writing less code.
I have been using key value Enum mapping a lot recently and would recommend it!
Here are a couple of examples!
Basic enum usage
enum InlineStyle {
"Bold",
"Italic",
"Underline"
}
type IS = keyof typeof InlineStyle
// Example of looping
(Object.keys(InlineStyle) as Array<IS>).forEach((key) => {
// code here
})
// Example of calling a function
const styleInline = (style: IS) => {
// code here
}
Enum key value usage
enum ListStyle {
"UL" = "List",
"OL" = "Bullet points"
}
// Example of looping
Object.entries(ListStyle).forEach(([key, value]) => {
// code here
})
Interface mapping
enum InlineStyle {
"Bold" = "isBold",
"Italic" = "isItalic",
"Underline" = "isUnderlined"
}
type InlineStyleType = Record<InlineStyle, boolean>
enum ListStyle {
"UL",
"OL"
}
type LS keyof typeof ListStyle
interface HTMLBlock extends InlineStyleType {
// This has extended with
// isBold: boolean
// isItalic: boolean
// isUnderlined: boolean
listType: LS
}
With ts-enum-util (npm, github), it's easy, type-safe (uses generics), and takes care of skipping the numeric reverse lookup entries for you:
import { $enum } from "ts-enum-util";
enum MyEnum { Hello, Goodbye };
$enum(MyEnum).map(v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // produces [':(', ':)']
NOTE: ts-enum-util always iterates based on the order of the sorted enum keys to guarantee consistent order in all environments. Object.keys() does not have a guaranteed order, so it's impossible to iterate enums "in the order they were defined" in a cross-platform guaranteed way.
(update: new version of ts-enum-util now preserves the original order in which the enum was defined)
If you are using string enums, then combine it with ts-string-visitor (npm, github) for even more generic type-safe compiler checks to guarantee that you handle all possible enum values in your map function:
(update: new version of ts-enum-util now includes functionality of ts-string-visitor, and it works on numeric enums now too!)
import { $enum } from "ts-enum-util";
import { mapString } from "ts-string-visitor";
enum MyEnum { Hello = "HELLO", Goodbye = "GOODBYE" };
$enum(MyEnum).map(v => {
// compiler error if you forget to handle a value, or if you
// refactor the enum to have different values, etc.
return mapString(v).with({
[MyEnum.Hello]: ':)',
[MyEnum.Goodby]: ':('
});
}); // produces [':(', ':)']
I would not call it general but I use this many times and may it will be handy for others too:
type TMyEnum = ':)'|':(';
class MyEnum {
static Hello: TMyEnum = ':)';
static Goodbye: TMyEnum = ':(';
}
console.log(MyEnum.Hello); // :)
console.log(MyEnum.Goodbye); // :(
Now you don't need any mapping function and it works as expected however you have to create separate similar class for every enum (which should not be a problem since you would do at anyway). The only drawback I can think now is that you can not iterate over it's properties. But until now it wasn't a problem for me I didn't need it. And you can add a static array to the class when you need it.
Maybe this will help you:
enum NumericEnums {
'PARAM1' = 1,
'PARAM2',
'PARAM3',
}
enum HeterogeneousEnums {
PARAM1 = 'First',
PARAM2 = 'Second',
PARAM3 = 3,
}
type EnumType = { [key: string]: string | number };
type EnumAsArrayType = {
key: string;
value: string | number;
}[];
const enumToArray = (data: EnumType): EnumAsArrayType =>
Object.keys(data)
.filter((key) => Number.isNaN(+key))
.map((key: string) => ({
key,
value: data[key],
}));
console.log(enumToArray(NumericEnums));
console.log(enumToArray(HeterogeneousEnums));
// Usage
enumToArray(HeterogeneousEnums).map(({ key, value }) => {
console.log(`${key}: ${value}`);
// Your necessary logic
return null;
});
Console result
This is a working function you can use. Below I'm passing ItemMaterial to getEnumKeys function and getting ["YELLOW", "WHITE", "ROSE", "BLACK"].
Similarly use the getEnumValues function to get values of the enum.
Take a look at the splitEnumKeysAndValues function to see how these variables extracted from the enum.
enum ItemMaterial {
YELLOW,
WHITE,
ROSE,
BLACK,
}
const keys = getEnumKeys<typeof ItemMaterial>(ItemMaterial)
const values = getEnumValues<typeof ItemMaterial, `${ItemMaterial}`>(ItemMaterial);
function getEnumKeys<TypeofEnum>(value: TypeofEnum): keyof TypeofEnum {
const { values, keys } = splitEnumKeysAndValues(value);
return keys as unknown as keyof TypeofEnum;
}
function getEnumValues<TypeofEnum, PossibleValues>(value: TypeofEnum): PossibleValues[] {
const { values, keys } = splitEnumKeysAndValues(value);
return values as unknown as PossibleValues[];
}
function splitEnumKeysAndValues<T>(value: T): { keys: keyof T, values: Array<string | number> } {
const enumKeys = Object.keys(value);
const indexToSplit = enumKeys.length / 2
const enumKeysKeyNames = enumKeys.slice(0, indexToSplit) as unknown as keyof T;
const enumKeysKeyValues = enumKeys.slice(indexToSplit);
return {
keys: enumKeysKeyNames,
values: enumKeysKeyValues,
}
}

Categories

Resources