Reputation: 59365
I am trying to compose functions in a way where I can define a name, input, and return types and then access them from a central function. However, when I do this I lose typing information.
How can I build a system like this without hard-coded value types?
import * as fs from 'fs';
import { promisify } from 'util';
import * as lodash from 'lodash';
const libs = []
export enum Types {
number,
numbers,
string,
buffer,
}
export enum Actions {
add,
subtract,
readFile,
}
libs.push({
action: Actions.add,
from: Types.numbers,
get: Types.number,
fn: (...n: number[]): number => n.reduce((a, b) => a + b, 0),
})
libs.push({
action: Actions.subtract,
from: Types.numbers,
get: Types.number,
fn: (...n: number[]): number => n.reduce((a, b) => a - b, 0),
})
libs.push({
action: Actions.readFile,
from: Types.string,
get: Types.string,
fn: async (s:string): Promise<string> => promisify(fs.readFile)(s, 'UTF8'),
})
libs.push({
action: Actions.readFile,
from: Types.string,
get: Types.buffer,
fn: async (s:string): Promise<Buffer> => promisify(fs.readFile)(s),
})
const library = (a: Actions, from: Types, get: Types, lib) => {
const found = lodash.find(lib, fn => {
return (
lodash.isEqual(fn.from, from) &&
lodash.isEqual(fn.get, get)
);
});
if (!found) throw new Error('no conversion');
return found.fn;
}
const { readFile } = Actions;
const { string: s } = Types;
const x = library(readFile, s, s, libs)
x('./tres.ts').then(console.log)
How can I retain typing information of x
?
Upvotes: 1
Views: 55
Reputation: 249656
We need to preserve in libs
the type of the actual items in the array. The easiest way to do this is with a help of an extra function that will infer the type for libs
based on the actual items in the array (including literal types for where Actions
and Types
are used).
With this information we can type the library
function to extract the type of the function from libs
that has the same action
, get
and from
as the passed in types:
import * as fs from 'fs';
import { promisify } from 'util';
import * as lodash from 'lodash';
export enum Types {
number,
numbers,
string,
buffer,
}
export enum Actions {
add,
subtract,
readFile,
}
function makeLib<T extends Array<{action : A, from: F, get: G, fn: (...a: any[])=> any}>, A extends Actions, F extends Types, G extends Types>(...a:T){
return a;
}
const libs = makeLib({
action: Actions.add,
from: Types.numbers,
get: Types.number,
fn: (...n: number[]): number => n.reduce((a, b) => a + b, 0),
}, {
action: Actions.subtract,
from: Types.numbers,
get: Types.number,
fn: (...n: number[]): number => n.reduce((a, b) => a - b, 0),
}, {
action: Actions.readFile,
from: Types.string,
get: Types.string,
fn: async (s:string): Promise<string> => promisify(fs.readFile)(s, 'UTF8'),
}, {
action: Actions.readFile,
from: Types.string,
get: Types.buffer,
fn: async (s:string): Promise<Buffer> => promisify(fs.readFile)(s),
})
const library = <T extends Array<{action : Actions, from: Types, get: Types, fn: (...a: any[])=> any}>, A extends Actions, F extends Types, G extends Types>(a: A, from: F, get: G, lib: T) => {
const found = lodash.find(lib, fn => {
return (
lodash.isEqual(fn.from, from) &&
lodash.isEqual(fn.get, get)
);
});
if (!found) throw new Error('no conversion');
return found.fn as Extract<T[number], {action : A, from: F, get: G }>['fn'];
}
const { readFile } = Actions;
const { string: s } = Types;
const x = library(readFile, s, s, libs) // x is (s: string) => Promise<string
x('./tres.ts').then(console.log)
const x2 = library(Actions.subtract, Types.string, Types.string, libs) // never
const x3 = library(Actions.subtract, Types.numbers, Types.number, libs) // (...n: number[]) => number
You can also use strings
instead of enums:
function makeLib<T extends Array<{action : V, from: V, get: V, fn: (...a: any[])=> any}>, V extends string>(...a:T){
return a;
}
const libs = makeLib({
action: "add",
from: "numbers",
get: "number",
fn: (...n: number[]): number => n.reduce((a, b) => a + b, 0),
}, {
action: "subtract",
from: "numbers",
get: "number",
fn: (...n: number[]): number | null => n.reduce((a, b) => a - b, 0),
}, {
action: "readFile",
from: "string",
get: "string",
fn: async (s:string): Promise<string> => promisify(fs.readFile)(s, 'UTF8'),
}, {
action: "readFile",
from: "string",
get: "buffer",
fn: async (s:string): Promise<Buffer> => promisify(fs.readFile)(s),
})
const library = <T extends Array<{action : string, from: string, get: string, fn: (...a: any[])=> any}>,
A extends T[number]['action'], F extends T[number]['from'], G extends T[number]['get']>(a: A, from: F, get: G, lib: T) => {
const found = lodash.find(lib, fn => {
return (
lodash.isEqual(fn.from, from) &&
lodash.isEqual(fn.get, get)
);
});
if (!found) throw new Error('no conversion');
return found.fn as Extract<T[number], {action : A, from: F, get: G }>['fn'];
}
const { readFile } = Actions;
const { string: s } = Types;
const x = library("readFile", "string", "string", libs) // x is (s: string) => Promise<string
x('./tres.ts').then(console.log)
const x2 = library("subtract", "string", "string", libs) // never
const x3 = library("subtract", "numbers", "number", libs) // (...n: number[]) => number
Upvotes: 2