Context:
I'm trying to write a function that will allow the user of the function to define a certain type using no typescript type assertions (just plain old javascript syntax). Basically, I'm trying to write something like React's PropTypes but I want to map the type defined with these "PropTypes" to a type that can be enforced with typescript.
Maybe it's easier to understand what I'm trying to do by seeing the code. Below defineFunction is a function that returns a function that takes in an object defined by "PropTypes"`.
interface PropType<T> { isOptional: PropType<T | undefined> }
type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
interface PropTypes {
string: PropType<string>,
number: PropType<number>,
bool: PropType<boolean>,
shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>
}
// the only purpose of this function is to capture the type and map it appropriately
// it does do anything else
function defineFunction<Props, R>(props: (types: PropTypes) => V<Props>, func: (prop: Props) => R) {
return func;
}
// define a function
const myFunction = defineFunction(
// this is the interface definition that will be mapped
// to the actual required props of the function
types => ({
stringParam: types.string,
optionalNumberParam: types.number.isOptional,
objectParam: types.shape(types => ({
nestedParam: types.string,
}))
}),
// this is the function definition. the type of `props`
// is the result of the mapped type above
props => {
props.objectParam.nestedParam
return props.stringParam;
}
);
// use function
myFunction({
stringParam: '',
optionalNumberParam: 0,
objectParam: {
nestedParam: ''
}
});
Here is the resulting type of myFunction found by hovering over the type in VS Code:
const myFunction: (prop: {
stringParam: string;
optionalNumberParam: number | undefined;
objectParam: {
nestedParam: string;
};
}) => string
Question:
There is an issue with the code above--optionalNumberParam is correctly defined as number | undefined but it is not actually optional!
If I omit the optionalNumberParam, the typescript compiler will yell at me.
Is there anyway to assert that a type is optional instead of just T | undefined?
Replying to cale_b's comment:
Just a thought - have you tried optionalNumberParam?: types.number.isOptional
Yes and it's invalid syntax:
And to clarify, this defineFunction should let the user define a type using no typescript type assertions--instead everything should be inferred using mapped types. The ? is typescript only. I'm trying to write this function so that--theoretically--javascript users could define proptypes and still have the typescript compiler enforce those proptypes.
So, closest workaround I can do involves that two-object-literal solution I mentioned:
interface PropType<T> { type: T }
First I removed isOptional since it does you no good, and second I added a property with T in it since TypeScript can't necessarily tell the difference between PropType<T> and PropType<U> if T and U differ, unless they differ structurally. I don't think you need to use the type property though.
Then some stuff I didn't touch:
type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
interface PropTypes {
string: PropType<string>,
number: PropType<number>,
bool: PropType<boolean>,
shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>
}
function defineFunction<Props, R>(props: (types: PropTypes) => V<Props>, func: (prop: Props) => R) {
return func;
}
Now, I'm creating the function withPartial, which takes two parameters of types R and O and returns a value of type R & Partial<O>.
function withPartial<R, O>(required: R, optional: O): R & Partial<O> {
return Object.assign({}, required, optional);
}
Let's try it out:
const myFunction = defineFunction(
types => withPartial(
{
stringParam: types.string,
objectParam: types.shape(types => ({
nestedParam: types.string,
}))
},
{
optionalNumberParam: types.number
}
),
props => {
props.objectParam.nestedParam
return props.stringParam;
}
);
Note how I split the original object literal into two: one with required properties, and the other with optional ones, and recombine them using the withPartial function. Also note how the user of withPartial() doesn't need to use any TypeScript-specific notation, which is I think one of your requirements.
Inspecting the type of myFunction gives you:
const myFunction: (
prop: {
stringParam: string;
objectParam: {
nestedParam: string;
};
optionalNumberParam?: number;
}
) => string
which is what you want. Observe:
// use function
myFunction({
stringParam: '',
//optionalNumberParam: 0, // no error
objectParam: {
nestedParam: ''
}
});
Hope that helps; good luck!
Related
I am trying to refactor my code to abstract redundant pieces to improve maintainability. I'm trying to create a single function that, depending on the parameter passed, will execute different smaller functions and return the necessary data.
For these smaller functions, they return an object whose properties come from a factory function, plus any additional properties I define that may or may not exist between all smaller functions.
const factoryFunc = () => ({
func1: () => 'func1',
func2: () => 'func2',
func3: () => 'func3',
})
const extendedFuncs1 = () => ({
...factoryFunc(),
additionalFunc1: () => 'additionalFunc1'
})
const extendedFuncs2 = () => ({
...factoryFunc()
})
I extracted the types return from these functions using the ReturnType utility. I wanted to get the keys for each available function, so I created a type that maps the keys to their respective function name.
type TExtendedFuncs1 = ReturnType<typeof extendedFuncs1>
type TExtendedFuncs2 = ReturnType<typeof extendedFuncs2>
type TFuncsTypes = {
extendedFuncs1: keyof TExtendedFuncs1;
extendedFuncs2: keyof TExtendedFuncs2;
}
Then, I created a conditional type to check if the property is of a certain function, and if it is, give the keys available for that function. The OtherType is for example only.
type TOtherTypes = {
otherType1: string;
otherType2: number
}
type Conditional<T = keyof (TOtherTypes & TFuncsTypes)> = T extends keyof TFuncsTypes ? {
name: T;
objKey: TFuncsTypes[T]
} : never
With this, I expected the objKey property to be the keys of the returned object of either TFuncsTypes['extendedFuncs1'] or TFuncsTypes['extendedFuncs2']. Additionally, if it is the TFuncsTypes['extendedFuncs1'], the additionalFunc1 property should exist.
const testFunc = (data: Conditional[]) => {
const findData = (key: string) => data.find((d) => d.name === key);
const res1 = extendedFuncs1()[findData('extendedFuncs1')!.objKey]
const res2 = extendedFuncs2()[findData('extendedFuncs2')!.objKey]
return {res1, res2}
}
However, typescript gives me an error for res2
Property 'additionalFunc1' does not exist on type '{ func1: () => string; func2: () => string; func3: () => string; }'
I am aware that it does not exist, as it is an additional property defined outside the factory, but why is it not getting evaluated to the keys defined in TFuncsTypes['extendedFuncs2']?
Here is a playground I made.
When you write the type Conditional[], you are referring to an array of the generic Conditional type where the type arguments take on their defaults:
type C = Conditional;
/* type C = {
name: "extendedFuncs1";
objKey: "additionalFunc1" | "func1" | "func2" | "func3";
} | {
name: "extendedFuncs2";
objKey: "func1" | "func2" | "func3";
} */
which is therefore a union type. Your findData function therefore returns a value of that union type (or undefined), no matter what key is:
const findData = (key: string) => data.find((d) => d.name === key);
const hmm = findData("extendedFuncs2");
/* const hmm: {
name: "extendedFuncs1";
objKey: "additionalFunc1" | "func1" | "func2" | "func3";
} | {
name: "extendedFuncs2";
objKey: "func1" | "func2" | "func3";
} | undefined */
For all the compiler knows, then, the result will have an objKey property of type "additionalFunc1". And that means it will complain if you try to index into the output of extendedFuncs2().
In order to fix this you need to express the relationship between the key string passed to findData() and the possible output type, so that the compiler understands that, for example, findData("extendedFuncs2")?.objKey cannot be "additionalFunc1".
One way to do this is to make findData() generic in the type K of key, and use the call signature for the find() method of arrays that returns a narrower result if its callback is a custom type guard function:
interface Array<T> {
// This is the call signature we want to use
find<S extends T>(
predicate: (this: void, value: T, index: number, obj: T[]) => value is S,
thisArg?: any
): S | undefined;
}
Like this:
const findData = <K extends string>(key: K) => data.find(
(d): d is Extract<typeof d, { name: K }> => d.name === key
);
Note that I have annotated the callback as a custom type guard function with the return type d is Extract<typeof d, { name: K}>; this lets the compiler know that a true result has the effect of narrowing the type of d from its original union type to just those members whose name property is of type K, using the Extract<T, U> utility type to filter the union.
Now when we call findData() we will see more specific results:
const hmm = findData("extendedFuncs2");
/* const hmm: {
name: "extendedFuncs2";
objKey: "func1" | "func2" | "func3";
} | undefined */
The compiler now knows that if objKey exists it will be a valid key of the result of extendedFuncs2(), and your error goes away:
const res2 = extendedFuncs2()[findData('extendedFuncs2')!.objKey]; // okay
Depending on your use case, this might be sufficient.
Playground link to code
I am trying to build a generic "req" function that takes an URL path (string), and object (GET/POST params) as arguments, and returns the results with the appropriate type inferred.
This would be the interface from where my functions gets its type defs:
interface ValidQueries{
books: (q: {
limit?: number;
category?: string;
}) => Book[];
authors: (q: {
name?: string;
}) => Author[];
}
And an attempt to write the function:
const req = <K extends keyof ValidQueries>(path: string, params: (Parameters<ValidQueries[K]>[0])) => {
return useQuery([path, params], ({ queryKey }) => {
const [, params] = queryKey;
return httpRequest(path, params) as Promise<ReturnType<ValidQueries[K]>>;
});
};
Example:
req("books", { limit: 10 }); -> return type should appear as Book[], and 2nd arg should appear as type of "q" from the matching function in the interface.
the params type definition does not appear to be correct. I wanted to take the type of the first param from the functions in my interface (q).
And the return type Promise<ValidQueries[K]> sort of works, but its not ideal because I see all possible types, like Book[] | Author[], instead of just Book[] if I pass "books" as first arg, or just Author[] if I pass "authors" as the first arg...
Most importantly, when you use generics you need to have some anchor, that TypeScript may infer from. In this function, you use path: string (so any string), and the only argument using generic K is params.
Because of that, when you call req("books", { limit: 10 }), to compute the generic K it will do something like Extract<ValidQueries[keyof ValidQueries], { limit: 10 }>. To fix that, you may use K for the path argument - path: K. That will help to find the proper method from ValidQueries.
It should also solve the problem with Book[] | Author[], unless the useQuery has incorrect types (then you may push your as … clause to the end, or type the function return type directly).
Additionally, you may use rest parameters like ...params: Parameters<…>, so it will allow passing any number of ValidQueries[K] arguments down. It will also add the q: name to the parameter in the IDE, that you expected.
Summing up, you have something like this:
interface ValidQueries {
books: (q: { limit?: number, category?: string }) => Book[];
authors: (q: { name?: string }) => Author[];
}
const req = <K extends keyof ValidQueries, T extends ValidQueries[K]>(path: K, ...params: Parameters<T>) => {
return useQuery([ path, params ], ({ queryKey }) => {
const [ , params ] = queryKey;
return httpRequest(path, params) as Promise<ReturnType<T>>;
});
};
It has a small adjustment though, that the T type is created here automatically, as an alias. You could use the full Parameters<ValidQueries[K]> form (and similarly for the return type).
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
}
};
},
});
I'v faced a problem trying to define an interface for the following structure:
interface JSONRecord {
[propName: string]: any;
}
type ReturnType = (id: string|number, field: string, record: JSONRecord) => string
export const formatDictionary = ({
mode = "render", key = "originalValue",
defaultKey = "originalValue"
}):ReturnType => (id, field, record) => {
...
}
interface Lookup {
Dictionary: ({mode, key, defaultKey}:{mode: string, key: string, defaultKey: string}) => ReturnType,
...
}
export const functionLookup:Lookup = {
Dictionary: formatDictionary,
...
}
export const formatField = (params:JSONRecord):string|ReturnType => {
const type:string = params.type
if (type === undefined) { return identity }
const fn = functionLookup[type]
if (fn === undefined) { return identity }
return fn({ ...params })
}
I'm getting the following errors:
In line const fn = functionLookup[type] : Element implicitly has an 'any' type becasue expression of type string can't be used to index type 'Lookup'. No index signature with parameter of type 'string' was found on type 'Lookup'.
I'm not really sure why is this happening, i thought that the Dictionary i defined in Lookup is supposed to be interpreted as a string. When i change Dictionary to [x: string]: ({mode, key, defaultKey}:{mode: string, key: string, defaultKey: string}) => ReturnType the error disappears, but i want to be specific with the arguments that can be passed.
In line return fn({ ...params }) : Expected 3 arguments, but got 1
I can't really wrap my head around this, the function is clearly expecting only 1 object as an argument {mode, key, defaultKey} or is it expecting the ReturnType function there?
I would appreciate any help. Thanks a lot in advance :)
In you case (from sandbox):
const anExampleVariable = "Hello World"
console.log(anExampleVariable)
// To learn more about the language, click above in "Examples" or "What's New".
// Otherwise, get started by removing these comments and the world is your playground.
interface Lookup {
test: number
}
const functionLookup:Lookup = {
test: 5
}
const params = {
type: 'test'
};
const type = params.type
const a = functionLookup[type]
params variable is infered as {type: string}.
Here functionLookup[type] you want use type as index for functionLookup, but TS does not work that way. Because you can't just use general type string as index for Lookup type.
Lookup allows you to use only literal test as index.
So you can add as const prefix to your params vvariable.
const params = {
type: 'test'
} as const;
You can make Lookup indexed:
interface Lookup {
test: number,
[prop:string]:number
}
Or, you can explicitly define a Record type for params:
const params:Record<string, keyof Lookup> = {
type: 'test'
}
Is there a way to set up a function based on an initial interface? I have a bunch of listOfFunctions and a an equal amount of interfaces that contain types of the functions. I would like to create a factory that returns a custom hook. The idea is to save a ton of code and make editing and adding features to all the hooks a breeze.
FunctionInterface Is all of the function names and their returns.
listOfFunctions is a setup function (the real code has a config) that returns a object with a list of functions.
theChosenFunction is a function that will be returned in the hook that allows me to interact with one of the listOfFunction functions. This means parameters (if needed). The functions in the real app are Promises that will set state of the hook. In this example, I just return a value and setState.
A) Is this possible to do with funcObject[funcName]() and create a function to invoke with typescript? This would need that ability to add params or not.
B) Is there a way to get the return value of a function type? So in this example:
type CanThisWork = () => string; I want to extract string; The idea is not to do a refactor on all of the listOfFunctions and FunctionInterfaces. There are a lot of them. If I have to create one complicated Factory, then I find this more economical.
interface FunctionInterface {
noParamsNoReturn: () => void;
noParamsNumberReturn: () => number;
propsAndNoReturn: (id: string) => void;
propsAndReturn: (id: string) => string;
}
const listOfFunctions = (): FunctionInterface => {
return {
noParamsNoReturn: () => void,
noParamsNumberReturn: () => 1,
propsAndNoReturn: (id: string) => console.log(id),
propsAndReturn: (id: string) => id,
}
}
function runThis<T>(funcName: keyof T, funcObject: T): void {
const [value, setValue] = React.useState();
// Can I have typescript set up the params here?
const theChosenFunction = ();
const theChosenFunction = (props) => {
// Can I have typescript invoke the function properly here?
const result = funcObject[funcName](); // funcObject[funcName](props)
if (result) {
setValue(result);
}
}
return {
theChosenFunction
}
}
const result = runThis<FunctionInterface>('noParamsNoReturn', listOfFunctions());
result.theChosenFunction()
// OR
result.theChosenFunction('someId');
There are built-in Parameters<T> and ReturnType<T> utility types that take a function type T and use conditional type inference to extract the tuple of parameters and the return type, respectively:
type SomeFuncType = (x: string, y: number) => boolean;
type SomeFuncParams = Parameters<SomeFuncType>; // [x: string, y: number]
type SomeFuncRet = ReturnType<SomeFuncType>; // boolean
As for your runThis() function,
I'd be inclined to give it the following typings and implementation, assuming you want the minimal changes that compile and run:
function runThis<K extends PropertyKey, F extends Record<K, (...args: any[]) => any>>(
funcName: K, funcObject: F
) {
const [value, setValue] = React.useState();
const theChosenFunction = (...props: Parameters<F[K]>): void => {
const result = funcObject[funcName](...props);
if (result) {
setValue(result);
}
}
return {
theChosenFunction
}
}
I've given the function two generic type parameters: K, corresponding to the type of funcName, and F, corresponding to the type of funcObject. The constraints K extends PropertyKey and F extends Record<K, (...args: any[])=>any> guarantee that funcName will be of a key-like type (string, number, or symbol), and that funcObject will have a function-valued property at that key.
Then, we can make theChosenFunction use spread and rest syntax to allow the function to be called with a variadic list of parameters. So (...props) => funcObject[funcName](...props) will accept any number of parameters (including zero) and pass them to the called function. TypeScript represents such lists-of-parameters as a tuple type. Therefore, having theChosenFunction's call signature look like (...props: Parameters<F[K]>) => void means that it will accept the same parameters as the entry in funcObject at the key funcName, and that it will not output anything (because your implementation doesn't output anything).
Let's see if it works:
const result = runThis('noParamsNoReturn', listOfFunctions());
result.theChosenFunction(); // okay
result.theChosenFunction('someId'); // error! Expected 0 arguments, but got 1.
const anotherResult = runThis('propsAndReturn', listOfFunctions());
anotherResult.theChosenFunction(); // error! Expected 1 arguments, but got 0.
anotherResult.theChosenFunction("someId"); // okay
runThis(listOfFunctions(), 'someId'); // error! '
// FunctionInterface' is not assignable to 'string | number | symbol'.
runThis('foo', listOfFunctions()); // error!
// Property 'foo' is missing in type 'FunctionInterface'
runThis(
'bar',
{ bar: (x: string, y: number, z: boolean) => { } }
).theChosenFunction("hey", 123, true); // okay
Looks good to me.
Playground link to code