#Can I make a non-function type that can be restricted by `typeof`
86 messages · Page 1 of 1 (latest)
Preview:```ts
type NonFunction<T> = T extends Function ? never : T
function foo<T>(value: NonFunction<T>): void
function foo<T>(value: () => string): void
function foo<T>(
value: NonFunction<T> | (() => string)
) {
if (typeof value === "function") {
console.log("function", value())
...```
yeah you can't really do negated types like this. can it be literally anything besides a function?
Yes the use case is like I want to define a function that takes either a T or a () => T as 2 overloaded definitions, kind of like react's useState, and I want to differentiate args in runtime using typeof while keeping typescript happy.
Specifically I am working on this helper for now, and I want to reduce the number of as es...
type NonFunction<T> = T extends Function ? never : T
type ForwardNullity<T, R> = R | (null extends T ? null : never) | (undefined extends T ? undefined : never)
/**
* `maybe` returns the result if the value is not null or undefined, otherwise forwards the nullity.
*/
export function maybe<V, R>(value: V, mapper: (value: NonNullable<V>) => R): ForwardNullity<V, R>
export function maybe<V, R>(value: V, result: NonFunction<R>): ForwardNullity<V, R>
export function maybe<V, R>(
...args: [value: V, mapper: (value: NonNullable<V>) => R] | [value: V, result: NonFunction<R>]
): ForwardNullity<V, R> {
const [value, mapperOrResult] = args
if (value === null) return null as ForwardNullity<V, R>
if (value === undefined) return undefined as ForwardNullity<V, R>
if (typeof mapperOrResult === 'function') {
return (mapperOrResult as (value: NonNullable<V>) => R)(value)
} else {
return mapperOrResult
}
}
why do you want overload signatures? nothing you've shown so far requires them; the implementation signature should be fine as the one and only signature
this is probably sufficient:
Preview:ts ... export function maybe<V, R>( value: V, mapperOrResult: (value: NonNullable<V>) => R | R ): ForwardNullity<V, R> { if (value === nul ...
Yes I'm just using overload signatures for readability, and this problem is not related to overloading anyway
actually i'd probably write mapperOrResult: R | ((value: NonNullable<V>) => R) to avoid confusion about operator precedence
overload signatures are unsafe, so i wouldn't use them just for that
(also personally i find them less readable, but YMMV)
Okay thanks for the advice and I may reconsider my view about overload. But is there really no better way to leverage typeof restricting T | (() => T)?
one problem with that whole idea is that functions are objects in javascript
it might seem like object | (() => Foo) should work, but object can still be a function
think about like the global Date object for example
should it be possible to have a mapper that returns a function in your code?
I was prepared to sacrifice a little bit of expressiveness by banning the mapper from returning a function and NonFunction<R> did behave that way, so for my current version the mapper cannot return a function.
Oops I tried again the current version of maybe does accept a mapper returning a function. It can be (value: NonNullable<V>) => NonFunction<R> to be more restrictive about that
this isn't the question you asked, but IMO this function is doing too many things. if you can restrict its scope then everything will be much simpler
if you're open to this kind of feedback, can you show me some examples of how you intend to use this function? i could give more specific suggestions with that in mind
Sure. Any suggestions are welcomed. I'm just trying to solve a common problem in my codebase that patterns like
someValue !== undefined ? getNewValue(someValue) : undefined
someValue !== null ? newValue : null
are verbose and error-prone. Using maybe they can be as simple as
maybe(someValue, getNewValue)
maybe(someValue, newValue)
and with ForwardNullity it preserves the null or undefined
Of course I can use separate function names for these 2 use cases, but I'd like to keep the concepts of my basic util functions as simple as possible, so I prefer using the same util function name.
And I think there're many other use cases when it would be helpful if I can have a function's parameter either a generic value T or a function returning it () => T, for all kinds of reasons. And the ability to use typeof differentiate them at runtime would be useful.
let's just focus on the "do something conditionally depending on whether a value is nullish" case first (but we can come back to that second thing)
i don't think i understand the motivation to make that generic (e.g. why ForwardNullity is needed). that would only be useful if the caller already knows that the value cannot be null/undefined, and in that case why would they call maybe at all?
for example, perhaps something like this would be good enough:
Preview:ts function maybe<T extends {}, R>( value: T | null | undefined, f: (value: T) => R ): R | null | undefined { return value === null || value === undefined ? value : f(value) }
If I have a value of type number | undefined and a mapper (v: number) => string, I expect a string | undefined instead of a string | null, and vice versa, just in case on the user's side there are some reasons to pick either null or undefined. Otherwise the user may need maybe(v, mapper) ?? null if maybe returns undefined by default. That's the motivation behind ForwardNullity
ah, so it's more about distinguishing T | null from T | undefined than it is T from T | null | undefined?
Yes I think so
sorry stepped away to make some coffee
can you tell me more about why you have to combine the handling for null and undefined in the first place? most codebases i see pick one or the other to represent "nothing"
Well on behalf of this maybe utility I think it's just trying to leave this to the user. It should does the job no matter if the user cares about the difference between null and undefined or not.
And I think it's quite beside the point 😂
well the function is going out of its way to have an opinion as to what values to handle specially. it could also decide to handle false, 0, '' or whatever else, but doesn't. it could also handle just undefined or null, but doesn't. it's all somewhat arbitrary, so i figured there might be a specific reason for this API
actually I'm using another utility for truthiness
export function given<V, R>(value: V, getter: () => R): R | undefined
export function given<V, R>(value: V, result: NonFunction<R>): R | undefined
export function given<V, R>(...args: [value: V, getter: () => R] | [value: V, result: NonFunction<R>]): R | undefined {
const [value, getterOrResult] = args
if (!value) return undefined
if (typeof getterOrResult === 'function') {
return (getterOrResult as () => R)()
} else {
return getterOrResult
}
}
is this part of a library meant for consumption by third parties? or just something you're using in your own codebase(s)?
It's just for my own codebase
yeah, then i think being opinionated is good. e.g. i typically choose undefined to model "nothing", so if there's a null floating around, it either has a specific meaning (and for example i would want mapper(null) to be called) or it's a bug (in which case i want a type error so i can figure out where the null is coming from)
but obviously this is all somewhat subjective
Yes I think everyone may have their own opinion about how undefined and null should be used, and that's something to be considered when coordinating multiple third party libraries. So the best thing I can do is to always keep the typings specific about nullity
how burdensome would it be to have separate maybeNull and maybeUndefined functions? then for example if you call a third-party function that may return either null and undefined to represent different things, you're forced to confront that rather than potentially mishandling one or the other
(99% of libraries will always either use one or the other though)
I was indeed handling null and undefined separately but later I found it tedious to choose one each time, and then I figure out ForwardNullity should solve this problem because it just preserves either null or undefined that was still actively generated somewhere else, so it just passes something that should have already been specific. Also I would like to keep the name of this utility as short as possible, because it's basically doing something that should ideally be done by some operator like ??, so it should have minimal impact on the logics it wraps. I know lots of people may be using someValue && newValue for cases like someValue !== null ? newValue : null, which does not differentiate nullity and truthiness well
yeah i've also wanted the equivalent && version of ?? before too
anyway, if you're set on these semantics, perhaps something like this:
function maybe<T, R>(
value: T,
f: (value: NonNullable<T>) => R
): R | Exclude<T, {}> {
return (value === null || value === undefined)
? value as Exclude<T, {}>
: f(value)
}
i don't think it's possible to write this without a type assertion or overloads, neither of which are typesafe
(i know that lacks the "R can be a bare value" thing. just was focused on the nullishness part here)
Actually I would like to hear your view about why overloads are not safe, I have always been thinking them as readability syntax sugars for complicated union parameters
here's an example:
function f(x: string): string
function f(x: number): number
function f(x: string | number): string | number {
return 42
}
f('oops').toUpperCase() // runtime exception, yet no type error
the function body is only checked against the implementation signature, but the overload signatures visible to callers may be more constrained
you may be interested in this upcoming language feature which is somewhat related: https://github.com/microsoft/TypeScript/pull/56941
that will allow writing some functions like this with conditional return types in a way that can actually be checked by the compiler
I see, yeah it's indeed unsafe
It seems working but I don't quite get the Exclude<T, {}> part?
{} means "any value that may have properties", i.e. "anything other than null or undefined"
so that picks out the null and/or undefined from T
it may be enlightening to learn that unknown is defined internally as {} | null | undefined
Thanks! That makes a lots of sense now, so even a boolean or a number can be assigned to some var of type {}, that's unexpected.
yeah, the notation is a bit odd when you think deeply about it. a string can also be assigned to { length: number }, which some folks might find odd
i kinda wish TS had chosen different delimiters so as not to suggest object-ness
Yes that would make it much less confusing
So my updated version of maybe is here
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
type NonFunction<T> = T extends Function ? never : T
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
type ForwardNullity<T, R> = Exclude<T, {}> | R
/**
* `maybe` returns the result if the value is not null or undefined, otherwise forwards the nullity.
*/
export function maybe<V, R>(
value: V,
mapperOrResult: ((value: NonNullable<V>) => R) | NonFunction<R>,
): ForwardNullity<V, R> {
if (value === null || value === undefined) return value as ForwardNullity<V, R>
if (typeof mapperOrResult === 'function') {
return (mapperOrResult as (value: NonNullable<V>) => R)(value)
} else {
return mapperOrResult
}
}
I really learned something about overloading and {}. @eager plover Thanks!
I'll leave this question open for some time in case anyone may have any clever ideas on optimizing the mapperOrResult as (value: NonNullable<V>) => R part but not with much hope🥲
personally i would not even have that NonFunction type. it doesn't really work the way you want and will just lead to confusion later
for example:
type NonFunction<T> = T extends Function ? never : T
function f<T>(x: NonFunction<T>) {}
const definitelyAFunction: unknown = () => {}
f(definitelyAFunction) // no error
I use a curried version of this pretty frequently, and semi-regularly think about trying to write it to handle either null or undefined, and then decide it's not worth the effort 😆
type MaybeType = <V, R>(fn: (value: V) => R) => (value: V | undefined) => R | undefined;
const maybe: MaybeType = (fn) => (value) => (value === undefined ? value : fn(value));
// usage
declare const s: string | undefined;
const n = maybe(parseInt)(s);
Yes you are right... really cannot trust stackoverflow lol
https://stackoverflow.com/questions/24613955/is-there-a-type-in-typescript-for-anything-except-functions
If you want any function, you'd use (...args: never) => unknown instead. Function is not a good type to use (even ESLint warns you about it 😄 )
@loud lynx what would you expect to happen if i call maybe('foo', Date)?
Yes normally I would follow eslint but this time I saw typeof did deduce something like & Function so I silenced that to see if there's any luck but still no😂
ideally a type error, but it seems hard to achieve
curried version looks great, and it kinds of encourages me to think if something like
maybe(isEven)(parseInt)(str)
is possible
I'm only loosely following the conversation, but imo most of the issue comes from the design decision that you want it to support both mapper or result.
It's a lot easier if you have two versions dedicated for each.
Yes for this specific function I guess so. Still I'm curious about the possibility to separately handle function / non-function for a generic parameter
I don't think it's possible without negated type.
Fundamentally the issue is that for T | (() => T), there's no way for you to tell which variant it is. typeof t === 'function' does not work because T could itself be a function.
not to mention the fact that there's fundamentally no way to do runtime narrowing of the parameter count/types. even (() => unknown) | ((x: unknown) => unknown) is impossible to narrow robustly
discriminated unions are usually the way to go for stuff like this (though that would undermine the goal in this case of reducing the amount of ceremony required at usage sites)
i guess length can be used for some cases?
Yes I think this is the answer
sorta. until you have a function that accepts a rest parameter, or uses the old arguments thing, or when there are two different functions that accept the same number of parameters but with differing types, etc
and TS doesn't narrow based on a .length check either, so you'd have to assert