#Alternative type for empty object
50 messages ยท Page 1 of 1 (latest)
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",
...```
use {} or object
It will then allow any value (like arrays).
I guess the merge won't work but the type error will not be at the right place :/
FWIW Record<string, never> allows arbitrary values too, because upcasting is always legal:
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)
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?
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
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
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
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
I mean "no additionnal properties needed"
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
Technically I don't want any other properties ^^'
But yes, you can always pass some to it and they will just be useless
Does T & object / T & {} will give you something else ?
If T always extend a declared object {name: string}, I guess this one is the stricter type and will be used ?
depends what T is. e.g. string & object is never, and undefined & {} is also never
Technically I don't want any other properties
why though? that's what i was wondering about up here
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 ๐
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
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
}>```
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
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
...```
Well looks like it works ๐
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
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
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)
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 voidunknown->everything
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 ballnever= 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 whatneverrepresents
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
Oh yeah you are right, was thinking of & as merging types like the spread synthax.
Didn't think about both condition should be true
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)
Well, I didn't really knew that, but it makes way more sense now.
I now know I don't know typescript ๐
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
I should probably take a deeper look at the doc to learn "the right way"