Implementing safe navigation in typescript - javascript

Recently I came across the following article in Medium about Using ES6's Proxy for safe Object property access from Gidi Meir Morris. I really liked it and wanted to give it a try in my Typescript project for optional nested objects without loosing type checking.
In order to turn optional nested objects into all-required, I'm using the following type:
export type DeepRequired<T> = {
[P in keyof T]-?: DeepRequired<T[P]>;
};
Gidi's code in typescript (including some hacks...):
export interface Dictionary {
[key: string]: any;
};
const isObject = (obj: any) => obj && typeof obj === 'object';
const hasKey = (obj: object, key: string) => key in obj;
const Undefined: object = new Proxy({}, {
get: function (target, name) {
return Undefined;
}
});
export const either = (val: any, fallback: any) => (val === Undefined ? fallback : val);
export function safe<T extends Dictionary>(obj: T): DeepRequired<T> {
return new Proxy(obj, {
get: function(target, name){
return hasKey(target, name as string) ?
(isObject(target[name]) ? safe(target[name]) : target[name]) : Undefined;
}
}) as DeepRequired<T>;
}
Usage example:
interface A {
a?: {
b?: {
c?: {
d?: string
}
}
},
b: boolean,
c?: {
d: {
e: number
}
},
d?: Array<{e: boolean}>
}
const obj: A = {b: false};
const saferObj = safe(obj);
Scenarios that it works without TS errors:
test('should work for nested optional objects', () => {
expect(either(saferObj.a.b.c.d, null)).toEqual(null);
expect(either(saferObj.a.b.c.d, undefined)).toEqual(undefined);
expect(either(saferObj.a.b.c.d, 322)).toEqual(322);
});
test('should work for required members', () => {
expect(either(saferObj.b, null)).toEqual(false);
});
test('should work for mixed optional/required tree', () => {
expect(either(saferObj.c.d.e, null)).toEqual(null);
});
As for arrays...
test('should work for arrays', () => {
expect(either(saferObj.d[0].e, null)).toEqual(null);
});
TS compiler throws the following error:
[ts] Element implicitly has an 'any' type because type 'DeepRequired<{ e: boolean; }[]>' has no index signature.
Any idea how can i make this work for Arrays?

Your code will work as is on Typescript 2.9 and above because in Typescript 2.9 the keyof operator includes numeric and symbol keys as well as string keys which were previously returned by keyof. Playground link
If you want to stick to 2.8 for some reasons, you can use the workaround of handling arrays explicitly in DeepRequired using a conditional type.
export type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends Array<infer U>?Array<DeepRequired<U>>: DeepRequired<T[P]>;
};

Related

Wrapping class in Proxy object TS

I am trying to write a function that accepts an API interface (I've created a sample here), and wraps a Proxy around it, so that any calls to the that API's methods get intercepted, and I can do some logging, custom error handling etc. I am having a terrible time with the types. This is similar to another question I have asked (Writing wrapper to third party class methods in TS), but uses a completely different approach than that one, based on some feedback I got.
Currently I am getting
Element implicitly has an 'any' type because expression of type 'string | symbol' can't be used to index type 'API'. No index signature with a parameter of type 'string' was found on type 'API'. which makes sense given that sayHello is not strictly a string as far as typescript is concerned, but I do not know the best way to be able to get methods on this class without uses the property accessor notation.
class API {
sayHello(name: string) {
console.log(“hello “ + name)
return name
}
}
export default <T extends API>(
api: T,
) =>
new Proxy(api, {
get(target, prop) {
if (typeof target[prop] !== "function") { // type error here with "prop"
return target[prop]; // and here
}
return async (...args: Parameters<typeof target[prop]>) => {
try {
const res = await target[prop](...args); // and here
// do stuff
return res
} catch (e) {
// do other stuff
}
};
},
});
Is this possible in TS?
TypeScript doesn't currently model the situation when a Proxy differs in type from its target object. There's a longstanding open feature request for this at microsoft/TypeScript#20846, and I don't know if or when the situation will change.
For now if you want to do this you'll need to manually describe the expected return type of your proxy function, and use type assertions liberally inside the implementation in order to suppress any errors. This means you'll need to verify that your function is type safe yourself; the compiler won't be able to help much.
Here's one possible approach:
const prox = <T extends API>(api: T) => new Proxy(api, {
get(target: any, prop: string) {
if (typeof target[prop] !== "function") {
return target[prop];
}
return async (...args: any) => {
try {
const res = await target[prop](...args);
return res;
} catch (e) { }
};
},
}) as any as { [K in keyof T]: AsyncifyFunction<T[K]> };
type AsyncifyFunction<T> = T extends (...args: infer A) => infer R ?
(...args: A) => Promise<Awaited<R>> : T;
The idea is that prox(api) returns a version of api where every non-function property is the same, but every function property has been changed to an async version that returns a Promise. So if api is of type T, then prox(api) is of mapped type { [K in keyof T]: AsyncifyFunction<T[K]> }, where AsyncifyFunction<T> is a conditional utility type that represents the transformation of each property type. If X is a function type with argument tuple A and return type R, then AsyncifyFunction<X> is (...args: A) => Promise<Awaited<R>>, using the Awaited<T> utility type to deal with any nested promises (we don't want a Promise<Promise<number>> to come out of this, for example).
Okay, let's test it:
class Foo extends API {
str = "abc";
double(x: number) { return x * 2 };
promise() { return Promise.resolve(10) };
}
const y = prox(new Foo());
/* const y: {
str: string;
double: (x: number) => Promise<number>;
promise: () => Promise<number>;
sayHello: (name: string) => Promise<void>;
} */
So, according to the compiler, y has a string-valued str property, and the rest of its properties are asynchronous methods that return promises. Notice that the promise() method returns Promise<number> and not Promise<Promise<number>>.
Let's make sure it works as expected:
console.log(y.str.toUpperCase()) // "ABC"
y.sayHello("abc") // "helloabc"
y.double(123).then(s => console.log(s.toFixed(2))) // "246.00"
y.promise().then(s => console.log(s.toFixed(2))) // "10.00"
Looks good!
Playground link to code
You can try add index signature to your class:
class API {
/* -- index signature -- */
[index:string|symbol]: any;
sayHello(name: string) {
console.log("hello" + name)
}
}
export default <T extends API>(
api: T,
) =>
new Proxy(api, {
get(target, prop) {
if (typeof target[prop] !== "function") {
return target[prop];
}
/* -- had to store it in a variable -- */
const data = target[prop];
return async (...args: Parameters<typeof data>) => {
try {
const res = await target[prop](...args);
// do stuff
return res.data;
} catch (e) {
// do other stuff
}
};
},
});

TypeScript strictly typed keys in a partial object

I'm using Mui components with TypeScript and trying to build a helper function to generate extended variants.
import { ButtonProps, ButtonPropsSizeOverrides } from "#mui/material";
declare module "#mui/material/Button" {
interface ButtonPropsVariantOverrides {
light: true;
}
}
type ButtonVariant = ButtonProps["variant"];
const buttonVariants: readonly Exclude<ButtonVariant, undefined>[] = [
"contained",
"outlined",
"light",
"text",
] as const;
I have actually had this working for a while using a key: string value for the object it returns:
const makeVariants = (): { [key: string]: () => void } => {
return {
light: () => {},
};
};
But I found a typo in one place, and that made me realize I should make this type-safe — instead of string (effectively any string as a key), I should require that the key be a member of the buttonVariants.
I naively tried this:
type VariantMakerKey = typeof buttonVariants[number];
const makeVariants = (): { [key in VariantMakerKey]: () => void } => {
return {
light: () => {},
};
};
But TypeScript is mad because I didn't return all the properties.
// Type '{ light: () => void; }' is missing the following properties from type
// '{ text: () => void;
// outlined: () => void;
// contained: () => void;
// light: () => void; }': text, outlined, contained
How can I make the object that returns only require a partial set of keys, so that if I define a key, it must be in buttonVariants, but I'm not required to declare them all?
Your mapped type constructs an object type containing all the members of buttonVariants. We have to find a way to make them optional to stop the compiler from requiring all properties to be set.
Their are certain Mapping Modifiers which can change the result of a mapped type. The ? modifier makes all properties optional.
const makeVariants = (): { [key in VariantMakerKey]?: () => void } => {
return {
light: () => {},
};
};
Playground

How to copy the structure of one generic to another generic, replacing the original values with custom values?

It may be helpful to read this question first: How to copy the structure of one generic type to another generic in TypeScript?
Given the following input type:
interface InputType {
age: number;
surname: string;
}
I'd like a function that can produce the following output type for the above input:
type OutputValue<T> = (val: T) => void;
interface OutputType {
age: OutputValue<number>;
surname: OutputValue<string>;
}
The signature of this function would be:
type OutputType<T> = {
[k in keyof T]: OutputValue<T[k]>
}
type TransformationFunction<Input, Output extends OutputType<Input>> = (input: Input) => Output;
The type signature above will ensure that the output is type-safe. Meaning I can use intellisense to correctly retrieve the output.surname function (for instance).
The tricky part is returning the correct data structure in a type-safe manner.
This is my attempt:
const transformString = (stringValue: string) => {
return (val: string) => {
// some code processing val
}
}
const transformationFunction = function<Input, Output extends GenericMap<Input>>(input: Input): Output {
const keys = Object.keys(input) as Array<keyof Input>;
return keys.reduce((output: Output, key: keyof Input) => {
const inputValue = input[key];
if (/*typeguard*/ isString(inputValue)) {
return transformString(inputValue); // typescript compiler complains
}
else if ( /*typeguard*/ isNumber(inputValue)) {
return transformNumber(inputValue); // similar function to above + typescript complains
}
else {
throw new Error("No transform");
}
}, {} as Output)
}
How can I apply my custom values to the output object?
Playground of actual problem
Using a for-loop instead of reduce, this becomes possible:
(output[key] as unknown as OutputValue<string>) = transformString(inputValue)
Instead of return transformString(inputValue) which expects OutputValue<Input[keyof Input]>
Playground link

Typescript, transform an object into another type while preserve its key in the output type

Suppose I write a code like this
type ResourceDecorator = (input: UserResourceDefinition) => DecoratedResourceDefinition
const decorate: ResourceDecorator = ...
const resources = decorate({
Book1: {
resourceName: 'my-book',
resourceType: 'book'
},
Pencil1: {
resourceName: 'my-pencil',
resourceType: 'pencil'
}
})
I want to write the function decorate(...) such that all first-level keys in the input type (Book1, Pencil1) are preserved in the output type. In other words, I want to use the output resources like this
// somewhere else
console.log(resources.Book1.resourceName)
// the decorate() function will add some programmatically
// defined properties in addition to the user definition.
console.log(resources.Book1.exampleDecorationProperty)
I have tried the indexable object syntax like this but it does not work.
export interface UserResourceDefinition {
[key: string]: {
resourceName: string,
resourceType: string,
}
}
export interface DecoratedResourceDefinition {
[key: string]: {
resourceName: string,
resourceType: string,
exampleDecorationProperty: string
}
}
type ResourceDecorator = (input: UserResourceDefinition) => DecoratedResourceDefinition
const decorate: ResourceDecorator = (input) => {
return Object.entries(definition).map(([resourceKey, userDef]) => ({
resourceName: userDef.resourceName,
resourceType: userDef.resourceType,
resourceKey: resourceKey,
exampleDecorationProperty: someFunction(userDef)
})).reduce((accumObj, decoratedDef) => ({ ...accumObj, [decoratedDef.resourceKey]: decoratedDef }), {});
}
It does not work because the type of the output resources does not know it has property Book1 and Pencil1.
// somewhere else
// The auto completion cannot infer 'resources.Book1'
console.log(resources.Book1.resourceName)
// The compiler does not complain about non-existing property 'Foo'
console.log(resources.Foo.resourceName)
It is possible to do this with Typescript?
So, what I'm suggesting is
const decorate= <T extends {}>(input:T) : T & {[key in keyof T]: {exampleDecorationProperty:string}} =>
{
let decorated = {...input}
const keys = Object.keys(input) as Array<keyof T>
keys.forEach(k => decorated[k]={...decorated[k], exampleDecorationProperty:'whatever'})
return decorated as T & {[key in keyof T]: {exampleDecorationProperty:string}};
}
If it won't work for you, I can delete this answer so you get better chance to get help from others :)

Typescript definition for union-type js

Would like to seek help writing d.ts file for https://github.com/paldepind/union-type
With the union type below,
let Maybe = Type({
Nothing: []
, Just: [Number]
})
I would love to see compiler error in case Maybe.Nothing() is mistyped as Maybe.None()
I tried to capture the keys from the object literal, but the compiler still fails to recognize Nothing and Just in the resulting type Maybe.
interface Obj {
prototype: any
case: (x: {[index: string]: (...args) => any}) => any
caseOn: (x: {[index: string]: (...args) => any}) => any
}
interface Union<T> {
(desc: T): T & Obj
}
var Type: Union<{[key: string]: any}>
export = Type
I think what you are looking for are Index Type, see the Advanced Types section of the handbook for more explanation on how they work.
I've also been making a declaration file for paldepind/union-type, but it's incomplete :
declare module 'union-type' {
type Constructors =
| StringConstructor
| NumberConstructor
| ArrayConstructor
| BooleanConstructor
| ObjectConstructor
| FunctionConstructor
export default function Type<
T extends {
[k: string]: (Constructors | ((arg?: any) => boolean | Constructors))[]
},
K extends keyof T
>(
desc: T
): {
// case(cases: { [k in K]: (...args: any[]) => void }, obj: any): void
[k in K]: (...args: any[]) => any
}
}
I'm also trying to find a workaround that library in TS...
If you are looking for a Maybe implementation here is one I wrote once upon a time
/**
* A Maybe implementation
* that is JavaScript first
* i.e. simple typed abstraction over null/undefined
*/
export class Maybe<T>{
private _value: T;
/** Based on value it will be Some or None */
constructor(value: T) {
this._value = value;
}
/** Shorthand for constructor */
static Some<T>(value: T): Maybe<T> {
if (value === null || value === undefined) {
throw new Error('value for some cannot be null or undefied');
}
return new Maybe(value);
};
static None<T>(): Maybe<T> {
return new Maybe(null);
};
get value(): T {
return this._value;
}
get isSome() {
return this._value !== null && this._value !== undefined;
}
get isNone() {
return !this.isSome;
}
map<U>(mapper: (now: T) => U): Maybe<U> {
if (this.isSome) {
return new Maybe(mapper(this._value));
}
else {
return new Maybe(null);
}
}
}
That said, I find it pretty useless, much simpler to be just aware of null/undefined and use valid property on your objects (more https://medium.com/#basarat/null-vs-undefined-in-typescript-land-dc0c7a5f240a)
More
That all said typescript is going to get first class nullability support https://github.com/Microsoft/TypeScript/pull/7140 and you will be able to do number | null | undefined and you would not be allowed to assign null to a number i.e. let foo:number = null; // Error foo is not nullable

Categories