#Alternative type for empty object

50 messages ยท Page 1 of 1 (latest)

molten olive
#

Hey there, is there a way to define an empty object without using Record<string, never>.
I am merging types with the object and when it is empty the never type prevent other attributes from being allowed.

fair spruceBOT
#
elliotyoyo#0

Preview:```ts
type Case1 = {
thing: string
}
type Case2 = Record<string, never>

type Merge1 = Case1 & {action: "case1"}
type Merge2 = Case2 & {action: "case2"}

const case1: Merge1 = {
action: "case1",
thing: "hello",
}

const case2: Merge2 = {
action: "case2",
...```

pallid field
#

use {} or object

molten olive
thick rune
#

FWIW Record<string, never> allows arbitrary values too, because upcasting is always legal:

fair spruceBOT
#
const f = (x: Record<string, never>) => {}

const x: {} = { foo: 'bar', arbitrary: true }
const y: {} = 'this is allowed'
const z: {} = ['this', 'is', 'also', 'allowed']

// no errors:
f(x)
f(y)
f(z)
thick rune
#

i'd have to know more about your particular use case to give specific suggestions, but generally in TS/JS you are better off focusing on what properties are allowed/used and ignoring any others that may exist

#

@molten olive what runtime behavior are you trying to model with your Merge* types?

molten olive
# thick rune <@280062818890547200> what runtime behavior are you trying to model with your `M...

My use case is basically to declare multiple action like that :

type Actions = {
  action1: { test: string }
  action2: {}
}

And then transform them to :

type AllActions =
  | {name: 'action1', test: string}
  | {name: 'action2'}

In my transformation, I use something similar to my first repro where I add { name: K } to each action.
Sometimes my actions don't have any additional data, sometimes they do and defining the ones without data with Record<string, never> break the transformation.


I guess using object works fine because the type merge will prevent any errors.
But my linter tells me it can cause issue since everything in JS/TS is an object.

#

At that point I'm just trying to please my linter which I should just ignore here

thick rune
#

yeah. also does it matter that you specifically have objects? is there a problem that happens if you do this?

type Actions = {
  action1: { test: string }
  action2: unknown
}
#

at the end if you're using AllActions you'll still be required to pass values with a valid name property

molten olive
#

Nope, using unknown is fine too, but I guess its a bit more explicit to use Record<string, never> or {} as "empty object" than unknown

thick rune
#

my question is: do you really mean "empty object"? i don't think so, for the same reason that { test: string } doesn't mean "an object that only has a property named test and is otherwise empty"

#

all values can always have excess properties at runtime

molten olive
#

I mean "no additionnal properties needed"

thick rune
#

unknown is the top type. it means "any arbitrary value", which i suspect is what you really mean by "no additional properties needed". it's more like "unspecified" or "unconstrained" than "empty"

#

and notably for the sake of what i imagine your transformation looks like, T & unknown is T for any T

molten olive
#

Technically I don't want any other properties ^^'
But yes, you can always pass some to it and they will just be useless

molten olive
#

If T always extend a declared object {name: string}, I guess this one is the stricter type and will be used ?

thick rune
#

depends what T is. e.g. string & object is never, and undefined & {} is also never

thick rune
molten olive
#

It's a declaration for an authorization system.
For example, if the user access a specific entity ( for example an Article), you want to pass the requested Article.Id as a param to check auth on it.
And for other things, you might not need any specific information.

My types are just the configuration, I want to declare the data needed (article ID) and since you don't need anything else, there is no point in providing something else.
But yeah, technicaly you could add more data and it will be ignored

#

Will go with unknown if it makes more sense. But it kinda feel like someone forgot to define the proper type ๐Ÿ™‚

thick rune
#

what does your actual merge operation look like? and is there ever actually a value of type Actions or does it only exist to derive other types from? i'm wondering if never would be more semantically clear

#

you'd probably need a conditional type in your merge type to make never work the way you want, though

fair spruceBOT
#
elliotyoyo#0

Preview:```ts
type Actions = "action1" | "action2"

type Wrapper<Data extends Record<Actions, unknown>> = {
[K in keyof Data]: Data[K] & {action: K}
}[keyof Data]

type ActionsDef = Wrapper<{
action1: {test: string}
action2: unknown
}>```

molten olive
#

Using never completely removes the action.
I tought of detecting the never type and directly return Data[K] instead of merging but didn't try

fair spruceBOT
#
elliotyoyo#0

Preview:```ts
type Actions = "action1" | "action2"

type Wrapper<Data extends Record<Actions, unknown>> = {
[K in keyof Data]: Data[K] extends never
? {action: K}
: Data[K] & {action: K}
}[keyof Data]

type ActionsDef = Wrapper<{
action1: {test: string}
action2: never
...```

molten olive
#

Well looks like it works ๐Ÿ˜„

thick rune
#

yeah that's what i was imagining

#

i'd personally probably just stick with unknown but if you think that's confusing i'd probably do the never thing as my second choice

#

the other options are just weird middle-grounds that don't really help clarify what you want to clarify IMO

molten olive
#

yeah, I like the never option better

#

Just sad we have to make a special case for it, not very intuitive that merging never & {name: string} gives never

thick rune
#

well never is the bottom type. it's specifically a type that has no possible values. and a value that cannot possibly exist intersected with anything is still a value that cannot possibly exist

#

in set theory terms never is an empty set of possible values. and the intersection of an empty set and any other set is an empty set

#

it's the dual of unknown, which is the set of all possible values

#

T | never is T for the same reason (and T | unknown is unknown)

molten olive
#

I really don't know set theory but my naive approch is :

  • Bob has 1 ball
  • Alice never have a ball
    -> They have a total of 1 ball ๐Ÿ™‚

Shouldn't it be like that then ?

  • never -> nothing / literally the void
  • unknown -> everything
thick rune
#

i'd probably say "anything" rather than "everything" for unknown, but yeah i think you're on the right track

#

maybe you're confused about what & means though. A & B means both A and B must be true. so it's more like:

  • T = Bob has 1 ball
  • never = it's impossible for Bob to have a ball
    "Bob has 1 one ball and it's impossible for Bob to have a ball" implies an impossible condition, which is what never represents
#

i dunno if this analogy is helpful though. i think Bob just muddies the waters ๐Ÿ˜†

#

const x: T & never = y means y must satisfy both the conditions described by T and the conditions described by never. the condition that never describes is "this value does not exist". and any possible condition & this value does not exist => this value does not exist

molten olive
#

Oh yeah you are right, was thinking of & as merging types like the spread synthax.
Didn't think about both condition should be true

thick rune
#

it does sorta merge sometimes. { a: unknown } is "any value with a property named a" and { b: unknown } is "any value with a property named b", so it makes sense that { a: unknown } & { b: unknown } is "any value with a property named a and a property named b", or { a: unknown; b: unknown }. but once the types you & together are incompatible in some way then it's not just a "merge" (and never is "not compatible" with any other type in this sense)

molten olive
#

Well, I didn't really knew that, but it makes way more sense now.
I now know I don't know typescript ๐Ÿ˜„

thick rune
#

this "types are sets of possible values" (or maybe "types are constraints") concept is a good bit of core knowledge that will help you understand lots of other stuff, but don't worry: nobody really "knows" all of typescript. i've got almost a decade of experience with the language and there are still plenty of things i need to try out in an editor rather than reasoning through in my head

molten olive
#

I should probably take a deeper look at the doc to learn "the right way"