hegel
hegel copied to clipboard
Excess property checks for destructured arguments
I've struck up a longer discussion about type-checking of destructured arguments in a Typescript issue here and I was wondering if I could get your opinion on this matter.
Given this example:
type User = {
id: number;
name: string;
email: string;
}
function stringifyUser({ name, email }: User): string {
return `${name} (${email})`
}
Would it be reasonable to infer the argument type as Pick<User, "name" | "email">
?
I mean, the underlying JS does not require an object with an id
property, so why should this be required by the type-checker?
Hegel tends to be about accuracy, and I had actually hoped to find it already working like this.
But as they pointed out in the thread, only 4 people have ever asked for this feature, so maybe it's just not something anybody notice, cares or thinks about. From my perspective, pretty simple: I want functions with as few dependencies as are actually required.
And sure, I could type out Pick
types all day, but why wouldn't the type-checker just check the types that are actually relevant to the function, instead of demanding properties that the function can't actually access?
That's how it works in JS, where destructuring arguments is the closest thing we have to a type-hint.
What do you think? 🤔
you are asking for User
type and you are getting User
and fact that you are not using all User
props is internal logic and can change in the future and should not cause breakage on the consumer side of that function
if you want to make that internal logic same as external interface in hegel you can write this
function stringifyUser({ name, email }): string {
return `${name} (${email})`
}
and inferred type will be what you wanted
<_a: { 'email': string, 'name': string, ... }>(_a) => string
you don't need to annotate anything type is derived from usage
this can be solved with AutoPick<T>
type like so
type User = {
id: number;
name: string;
email: string;
}
function stringifyUser({ name, email }: AutoPick<User>): string {
return `${name} (${email})`
}
so it's same as Pick<User, "name" | "email">
type but second argument is derived from destructuring logic
i don't see this implemented in typescript or hegel anytime soon but it can be done
you are asking for
User
type and you are gettingUser
and fact that you are not using allUser
props is internal logic and can change in the future and should not cause breakage on the consumer side of that function
Is it "internal logic" though?
I mean, it's part of the function declaration - it's the closest thing we have to a type-hint in JS, in practice saying, "the argument is an object with these properties".
I think it's a matter of description. The way I see it, the fact that an object is required, and the fact that the object must have certain properties, that's not just internal logic - it's part of the function's public interface. These are declarations with static meaning.
It's a close analog to named arguments, as well as (effectively) a shallow run-time type-check.
Adding new properties to a destructured argument is not that different from adding new arguments to a function - unless they're optional, calling the function without those arguments should be an error.
And vice versa, calling the function with extra arguments is permitted, just as calling the function with extra properties to destructured arguments is permitted.
But not required.
Why would it be? 🤔
Let me reverse the question:
Where do you see a case for functions that demand properties they can't access or use? 🤷♂️
i understand where confusion comes from but destructuring is not part of function interface
it's just a suger syntax
// original
function stringifyUser({ name, email }) {
return `${name} (${email})`
}
// desugering pass 1
function stringifyUser(obj) {
const { name, email } = obj
return `${name} (${email})`
}
// desugering pass 2
function stringifyUser(obj) {
const name = obj.name
const email = obj.email
return `${name} (${email})`
}
this is what js engines do on AST level before executing that function
so it is functions internal logic and has nothing to do with types
Let me reverse the question:
Where do you see a case for functions that demand properties they can't access or use? 🤷♂️
btw they can access it
type User = {
id: number;
name: string;
email: string;
}
function stringifyUser({ name, email }: User): string {
return `${name} (${email}) - ${arguments[0].id}`
}
i understand where confusion comes from but destructuring is not part of function interface
it's just a suger syntax
I promise, I'm not confused. 😄
What you explained is just language implementation details. How the language or some transpilers implement this feature internally is rather moot. I'm talking about what the language feature means to programs and programmers in a practical sense.
If you asked for { name, email }
, you are asking for an object - both at run-time and statically, you've declared a function that accepts only objects with those properties. That's what this feature means in practice, to programmers.
btw they can access it
I also noted that in my original post:
This being JavaScript, you can of course access the object using
arguments[0]
- so the type of this object could in fact matter.However, the type of
arguments[0]
is alwaysany
, and so, in that case, there's no type safety either way; if the rest of the type is important, you probably aren't (and likely shouldn't be) destructuring the argument in the first place.So this seems unlikely to affect anything other than things like currying and higher-order functions, where you wouldn't be destructing anyway - and so, it seems unlikely this will cause any adverse effects in practice.
Can you think of a practical example where this would be problematic?
Let's get to the bottom of this. 😄
If a function declaration explicitly declares a dependency on a subset of properties of an object:
When, how or why is it useful to get a resulting function type that validates statically for the presence of properties that the function does not ask for or expect at run-time?
If you asked for
{ name, email }
, you are asking for an object - both at run-time and statically, you've declared a function that accepts only objects with those properties. That's what this feature means in practice, to programmers.
you are not asking for { name, email }
that part is internal to the function
you are asking for User
and that is what any type checker is going to check for
if you want usage based interface on functions hegel does do that but in that case you need to remove annotation for User
type
Can you think of a practical example where this would be problematic?
i have a function that's asking for User
object and current implementation does not use email
prop but i want consumers of this function to pass full User
objects so when i change how my function works and start using email
prop consumers don't need to update how they use this function
you are not asking for
{ name, email }
that part is internal to the function
That's a matter of interpretation.
If you have a function that accepts { a, b }
today, and tomorrow it requires { a, b, c }
, that's a breaking change to the emitted JS code - regardless of how you type-hinted it in Hegel.
So the way I see it, it's not internal to the function - it's part of it's public signature.
I mean, if you wanted to be really pedantic, you could argue that the whole argument list is internal to the function, right? Technically, a function (a, b)
can be called with just (a)
or with (a, b, c)
etc. - JavaScript won't complain, arguments
is just a list of the arguments that were given.
you are asking for
User
and that is what any type checker is going to check for
In a nominally-typed language, yes - but in a structurally-typed language?
What you really asked for is "object matching this shape" - and then explicitly selected certain specific properties from that shape. If we have these facts, why would we type-check properties you didn't ask for? Unless you also asked for ...rest
, the reality is a function that accepts, and works correctly for, any object with a name
and email
property.
Can you think of a practical example where this would be problematic?
i have a function that's asking for
User
object and current implementation does not useUser
objects so when i change how my function works and start using
That sounds more like an imagined future problem than a practical problem.
If you anticipate a future need for a full User
instance, you probably shouldn't destructure - again, since type-hints are elided, when you destructure, the emitted JS is code that does not need the full object. If your function really depends on a full User
instance, you would ask for the full instance.
Anyhow, I think I've made my point - I think this feature solves a problem, but no one seems to agree with me. 😅