3
const fn = () => null
type Answer = typeof fn extends Record<string, any> ? true : false

The snippet above produces Answer to be of type true, indicating that a function is considered a subtype of Record. Why?

How can I make my types more strict so that functions are not its subtype?

2
  • 1
    "How can I make my types more strict"... which types, exactly? Probably whatever you have can be made more strict than Record<string, any> Commented Jun 28, 2018 at 17:59
  • I just want things that are objects, but are not callable. It's for a very global validator function and checking if something is an "object" (a record) is done in order to recursively traverse the tree of validators. I guess the question is "how to type a Record which is not a function?". I tried Exclude<Record<string, any>, Function> but that still allows a function. Commented Jun 28, 2018 at 20:34

2 Answers 2

7

So, according to your comment, you want something like "an object which is not a function". TypeScript doesn't currently have negated types, so there's no concise way to express "not a function". Depending on your use case, you might just want to shrug and move on.

Or, you might try to force TypeScript to bend to your will with a workaround.


The simplest workaround is to find some property that functions always have and declare that your type has an incompatible optional property of the same name. For example:

type NoCall = { call?: number | string | boolean | null | undefined, [k: string]: any };
const okay: NoCall = { a: 4 }; // okay
const err: NoCall = () => 2; // error

A drawback is that you'd lose the ability to validate some legitimate non-functions (in the above case, {call: ()=>0} is not a function but would fail to validate as a NoCall). But maybe some false positives are okay for your use case?

Anyway, reasonable choices according to the standard library lib.es5.d.ts for property names to check are apply, call, bind, arguments, and caller.


If you don't like giving up one of those names, you could optionally use global augmentation to add a phantom property to the Function interface:

declare global {
  interface Function {
    '*FunctionsHaveThis*': true
  }
}
type ObjNotFunction = { '*FunctionsHaveThis*'?: never, [k: string]: any };
const okay: ObjNotFunction = { a: 4 }; // okay
const err: ObjNotFunction = () => 2; // error

which makes property name collisions less likely but pollutes the global namespace and makes this workaround considerably less simple.


Another workaround is to use a trick with conditional types which prevent a function from being passed as a parameter to another function:

function noFunctionsPlease<T extends object>(t: T & (T extends Function ? never : T)) {
  // do something with t
}
noFunctionsPlease({ a: 4 }); // okay
noFunctionsPlease(() => 2); // error

The type of t is more or less exactly saying "an object that isn't a function", but it can only be expressed as a function argument.


Hope one of those helps you or gives you an idea about how (or whether) to proceed. Good luck!

Sign up to request clarification or add additional context in comments.

1 Comment

Wow, thanks a lot! I like the call one. Already did a workaround by switching around conditions so I ask for everything except the object and then simple leave that as the "else" case... Time to refactor that again \m/
3

Record<string, any> indicates any object that has any string key and any value. All functions are objects, and they have some properties ( we can even index into the object fn['call']). By these definitions, and since type relations in Typescript are based on structure, typeof fn does extend Record<string, any>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.