#Intersection to define OOP Types?

1 messages · Page 1 of 1 (latest)

sharp kiln
#

Typeof() works fine but when I use intersection it doesnt work?

The error says "Expected this to be '(methods & self) -> ()' but got '(methods & self) -> ()'"

Looks like the same type to me?

I mean I can just "fix" it by casting self "local self = setmetatable({} :: self, Car) :: Car" but that seems wrong and I dont know why it works

forest spoke
#

ur adding too many unneccessary types vro

#

js do one type named car

#

with the speed and the boost method

#

alsi

#

in the method

#

do self: Car

#

and when u define self

#

type cast the set metatable at the end to car

#

:: Car

#

also

#

in the definition

#

cast the table to any

#

not self

#

@sharp kiln

sharp kiln
# forest spoke cast the table to any

yea that does seem like less hassle ngl im just following this tutorial why would they go through the effort of typing self when you can cast any to it?: https://devforum.roblox.com/t/guide-to-type-checking-with-oop/1997394

forest spoke
#

dumbass

#

prob

sharp kiln
#

also whats the difference between just using typeof and intersection for defining your type?

#

cause in the error they look the exact same but it complains

forest spoke
#

intersections r prone to dependency errors

sharp kiln
forest spoke
#

nah

#

type checker js cant see them as a single object

sharp kiln
forest spoke
#

ye they are merged

#

but luau still sees them

#

as a static shape

sharp kiln
#

oh ok so should I just avoid them then?

#

and just stick to typeof()

forest spoke
#

no

#

depends

sharp kiln
#

I guess for normal types it would be fine I guess I think just cause of the metatables in oop it kinda bugs out

forest spoke
#

oop sucks for luau

sharp kiln
#

yea Its kinda a crutch for me lol

forest spoke
#

they should fr improve it

sharp kiln
#

also have u encountered cyclic type dependencies before? ive tried to fix it by just have a module table for those types is that fine?

#

but its kinda annoying since I have to use intersection since I dont want to pass in each table for their types

forest spoke
#

ye i have

forest spoke
sharp kiln
#

or should it just be like one object without intersections

sharp kiln
#

also this was the better method right?:

coarse root
#

that just ruins the concept of OOP

#

ur meant to use methods for Classes

#

but ig sure u can use dot

sharp kiln
coarse root
#

Ik that

sharp kiln
coarse root
#

it doesnt make sense

#

if ur not using method

#

i rather do Car:Drive() rather than pass self manually

coarse root
#

and cast this to module

#

and done

#

now u got self

#

with type

#

while using methods

sharp kiln
coarse root
#

ur meant to cast it to ca

#

car

#

idk if it works for classes

#

and gng why strict mode

#

💔

sharp kiln
#

why is strict mode not good?

#

im tyrna learn it rn I dont know anything

coarse root
#

ok

wary radish
coarse root
#

ur not meant to use strict with classes

viscid boneBOT
#

studio** You are now Level 5! **studio

coarse root
#

obviously strict mode will error it

sharp kiln
#

so I shouldnt use strict mode on classes where would I use it?

wary radish
#

typestrictness be like: lua --!strict type myclass = any

coarse root
#

if u really want to on anything that isnt a class

sharp kiln
#

ok so if im type checking I should nonstrict for classes?

#

and if I want I can use strict for the children

coarse root
#

on top

wary radish
#

classes are fine

sharp kiln
coarse root
#

for example if u use vsc u shouldnt use strict mode as many errors will come as vsc doesnt know what is the workspace etc

sharp kiln
#

ok ill try that thanks

#

👍

coarse root
#

oh yea and use vsc

#

not studio

#

it comes to preferences but vsc offers many things than studio like better auto completion etc

#

i only got 2 months of experience on scripting tbh so you could get different answers from scripters with more experince

wary radish
#

i respect that

coarse root
#

ty

wary radish
#

but the why is all over the place

wary radish
coarse root
#

ppl just uses strict mode and think their now good

wary radish
wary radish
#

vsc generally isn't worth it unless you're working in a large team, even with scriptsync

coarse root
#

also allows to save on git

wary radish
#

small team to solo you don't need all that extra bloat

wary radish
sharp kiln
#

actually probs longer

#

ive learnt a lot so im happy making progress

#

I still dont know if its worth all the even tho ngl

#

cause im solo but I just wanted to try it for fun

wary radish
#

ya so, most people here have no clue how to build metatable objects correctly, far less how to typecheck correctly

#

and even fewer know how to build a typechecked inherited metatable object

#

well, how to build it correctly

sharp kiln
#

how did u learn how to do it did u find some guide somewhere? or just from normal practise

wary radish
#

practice

#

i just made stuff with it

#

research and investigated it

#

etc

#

trial stuff out

coarse root
#

the difference beetween a beginner and experienced scripter is that they think different they got the same knowledge but one thinks better than other

wary radish
#

we... we do not have the same knowledge

coarse root
#

wym

#

idk tbh i read it online lmao

#

but i think beginners shouldnt be ashamed to look online for answers

#

i got 2 months and i still search when confused

wary radish
#

anyway so as far as typechecking goes, you should not use it for absolutely everything. some types cannot be properly typechecked, and you must typecast. e.g when pulling in data from a datastore, it will be an unknown type. you have to refine and assert it to be the type you were expecting. so just based on this fact alone we know that not everything can be strictly typechecked.

#

it's the same as putting all of your code under an actor - it's just overkill

#

and makes things harder than it needs to be

#

specially considering sometimes you physically do not have the type it starts with

#

so that leaves the question of where do you use it

sharp kiln
wary radish
#

strict is fantastic for small, self-contained, and very well-defined things. like a signal module. there's no reason not to use strict for simple stuff.

#

you can even use it for metatable objects

#

-- as long as those metatables are simple, small, self-contained etc

#

if your type is more complicated, like it needs to inherit large and complex types, it may be better to use nonstrict.

sharp kiln
wary radish
#

e.g in my game i have a session class that goes through like 6 steps and has a few different branches of what the type can be - e.g the localplayer's session, server session, or general any-player session etc

#

i could probably make that typestrict

#

but i really don't care enough about having the types accurate to the data for a structure as large and complex as this.

#

so instead i use the types exclusively for its autocomplete / annotations. they are not exact representations of exactly what the data is, but it's close enough that it doesn't matter, since i only care about the autocomplete and other hints.

#

so, that's how i've found typestrictness to be used best

#

that said

#

as for the ACTUAL metatable objects

#

yall doing this shit so wrong its insane spanishkek

sharp kiln
wary radish
#

for starters, before literally anything else, you cannot use intersections in the metatable.

#

type x = setmetatable<{}, A & B> this does NOT work

#

for whatever reason the luau typesolver refuses to resolve those

#

type x = setmetatable<A & B, {}> is fine though.

#

so

#

every method you've used or read about up until this point is pretty much wrong for this reason.

#

none of them are typesafe (not accurate representation of what the data really is)

#

to do unions typesafely

#

without actually using unions in the metatable

#

... it gets a little tricky but essentially you need to use true metatable inheritance

#

which is basically, give the metatable's index table its own metatable

#

which is how metatables are supposed to work in lua.

#
setmetatable({}, {__index = setmetatable({new_index=0}, {__index= setmetatable({new_index=1}, {__index={empty_final_index}} }) }) })``` you get the idea
#

the index can itself have a metatable.

#

and when you index from the root, it will check the first index layer, then the second, and so on

#

that's how you do metatable inheritance correctly.

sharp kiln
#

ok gimme a sec to process this

wary radish
#

now this works just fine for one layer

#

you might even get to 2 layers, but it starts to get very complicated after 3, specially if you have different subclasses

#

why?

#

because the typesolver cannot properly identify the parent class from the child class, or visa-versa

#

essentially, the types are wrong

#

like doing local x:number x="foo" the type is just not a match

sharp kiln
# wary radish now this works just fine for one layer

so if I have

Vehicle
LandVehicle
Car

and they were inherited from their parent you would have to use that "setmetatable({}, {__index = setmetatable({new_index=0}, {__index= setmetatable({new_index=1}, {__index={empty_final_index}} }) }) })" ?

wary radish
#

Essentially you MUST use generics so that the baseclass can 1) know there exists a child class, and 2) the child class can identify itself in the parent, and 3) the baseclass and the childclass use the exact same type.

#

point 3 there is important. if the baseclass and childclass are not the exact same type, it won't resolve correctly

#

now you might be wondering how on earth tf do you do that, when the child class might be anything?

sharp kiln
#

ok so u have to use generic for inheritance since the type checker has a hard time telling the types of the child and parent

wary radish
#

again - Generics.

wary radish
#

the types are just simply wrong; they don't match.

#

when you define the index for the baseclass, you pass in a particular type for 'self'. if that type is not the same as the 'self' used by the child class, then the baseclass methods won't be visible - they'll essentially be a type error.

#

because you're, in essence, trying to pass in a number to something expecting a string. thats what i mean by they're just fundamentally different types.

sharp kiln
#

im a little bit confused when you say to define the index for the baseclass

#

so you have the baseClass and then u pass self to it however it needs to be the same as the child class and there will be a type error

wary radish
#

the correct way is local meta={} local metaindex={} meta.__index=metaindex notice how they are in fact different tables

sharp kiln
#

ohhhh

wary radish
#

but most people are lazy and just do it the cheats way

sharp kiln
#

ok so ill try refresh:

1.dont use intersection to define metatables unless its in that swapped format since they dont actually represent the data of the metatable
2.give metatables index table its own metatable so that when u index from the root it will go down the chain of its children's types
3.use generics so that baseClass knows about the child and the child knows about the parent
4.the baseClass and child dont know about each others types since they both use a different self

sharp kiln
wary radish
#

so for starters, to do this properly

#

this is what a correctly type-defined, fully expanded metatable type looks like: lua type my_Data = { foo:string; } type my_Index = { dostuff:(my_Class)->() } type my_Meta = {__index:my_Index} type my_Class = typeof(setmetatable({}::my_Data, {}::my_Meta))

#

notice how this is in fact a recursive type

#

since my_Index needs the type of the final metatable in order to accurately define 'self', but the final metatable needs the my_Index type.

#

and what i'm saying is if you really, really want to use typesafe objects, and correctly define them

#

ya this is just the first layer. i haven't gotten to the generics yet and it's already a recursive type.

#

let's duplicate this for the child class

#
type my_Data = { foo:string; }
type my_Index = { dostuff:(my_Class)->() }
type my_Meta = {__index:my_Index}
type my_Class = typeof(setmetatable({}::my_Data, {}::my_Meta))

type child_Data = { bar:string; }
type child_Index = { dochildstuff:(child_Class)->() }
type child_Meta = {__index:child_Index}
type child_Class = typeof(setmetatable({}::child_Data, {}::child_Meta))``` from here we need my_Class to "know" about child_Class, and we need the child_Class to "know" about my_Class
#

we also need to construct the metatable correctly to avoid the, no-intersections-in-the-metatable problem

#

which looks like this

#
type my_Data = { foo:string; }
type my_Index = { dostuff:(my_Class)->() }
type my_Meta = {__index:my_Index}
type my_Class = typeof(setmetatable({}::my_Data, {}::my_Meta))

type child_Data = { bar:string; }
type child_Index = { dochildstuff:(child_Class)->() }
type child_Meta = {__index:typeof(setmetatable({}::child_Index, {}::my_Meta}))} -- __index=setmetatable(this_data,baseclass)
type child_Class = typeof(setmetatable({}::child_Data & my_Data, {}::child_Meta))``` so this is the first step
#

but it's not done yet

#

because while the child inherits from the baseclass, the baseclass won't refine properly since you're essentially calling baseclass(child_object) but the baseclass's index doesn't have that definition for self.

#

{ dostuff:(my_Class)->() } the my_Class here needs to be something that can be either my_Class, or child_Class at the same time, somehow.

#

that's done with generics.

#

we also need to pass up the data members

#

that might look a bit like this

wary radish
# wary radish Essentially you MUST use generics so that the baseclass can 1) know there exists...
type my_Data = { foo:string; }
type my_Index<T,C> = { dostuff:(my_Class<T,C>)->() }
type my_Meta<T={},C={}> = typeof(setmetatable(C, {__index:my_Index<T,C>}))
type my_Class<T={},C={}> = typeof(setmetatable({}::my_Data & T, {}::my_Meta<T,C>))

type child_Data = { bar:string; }
type child_Index = { dochildstuff:(child_Class)->() }
type child_Meta = typeof( setmetatable({}::{
  childFunc:(self:child_Class)->();
}, {__index=my_Index<child_Data,child_Meta>}))
type child_Class = my_Class<child_Data,child_Meta>``` and with this step, we've created this complex doubly recursive type that meets all the conditions i described earlier. The child class uses the same type as the baseclass (both are my_Class but the child has different values for the generics), the baseclass knows there exists a child class, and the child class can identify itself in the parent.
#

now

#

if you want to take this and make it infinitely recursive

#

so you can baseclass -> childclass -> subclass -> etc

#

i leave that as a challenge to you

#

I have in fact built one

sharp kiln
#

oh my god bro thanks so much I actually understand it

wary radish
#

but it was at that point i said fuck types

wary radish
sharp kiln
#

Im a bit confused at the generics but I tihnk I can just spend some time looking at thie post

#

to figure it out

wary radish
#

ya so like, you're going to have a lot of problems getting this to refine properly if you don't understand the recursive type

#

but the main trick is that all child classes use the same type as the baseclass type my_Class<T={},C={}> = typeof(setmetatable({}::my_Data & T, {}::my_Meta<T,C>))

sharp kiln
#

and just one last question sorryI dont wanna keep annoying you youve been a great help just for cyclic dependencies lets say u got:

State
StateMachine

Type module -- I assume you would have to pass the State and StateMachine tables into the types modules if they both need each others types right?

wary radish
#

so uh just like how the metatables are a recursive type

#

you can use recursive types elsewhere

#
type A= B & {}
type B= A & {}```
#

or lua type A= {foo: B} type B= {bar: A} type union=A&B

#

etc etc

sharp kiln
#

oh yea u dont require the actual table

wary radish
#

if you have 2 types that depend on each other (recursive type) then they need to be in the same file, or use generics

#

note that generics will not allow you to refine to what the generic is without some extra work

#

basically meaning, wherever you use A, if B is passed in by a generic, then A will know B "exists" but it won't know much of anything about the type actually passed in, unless your function has refined the generics

#

e.g myfunction(thinga:A, thingb:B) as opposed to myfunction<B>(thinga:A, thingb:B) in this second case, myfunction won't know what B is.

#

so ya

#

there's a reason why you're finding it so hard to get this right

wary radish
#

and those who do know have no reason to tell you.

#

in fact only reason i'm telling you is because you've been at it for 3 days and i feel bad for you

sharp kiln
#

ill try implement it and se ehow far I get

wary radish
sharp kiln
#

👍

wary radish
sharp kiln
#

hahhaa

wary radish
wary radish
sharp kiln
#

yea fair enough

wary radish
#

it is such a massive waste of time on something that has no real performance benefit, adds a tonne of extra work and technical debt

#

in conclusion, use strict for small self-contained things. use nonstrict for large complex things.

#

and i can assure you

sharp kiln
#

thanks again man you've been a great help

wary radish
#

the nonstrict technique isn't really any easier, particularly if your type is complex enough

#

you still need generics to define subclasses, etc. it's just .. you don't have those extra steps dealing with the stupid metatables-cannot-be-intersections

#

you still need all the child classes have the same type, baseclass<childclass_type_data>

#

it's just

#

...

#

slightly easier to work with

sharp kiln
#

yea I mean if u got like 4 leves of inheritance then wont ur type defintion be like half ur whole script at that point

wary radish
#

and for the record, my session class is between 4 and 6 steps.

sharp kiln
#

oof

#

would u try to stay away from oop then for more complex inheritance so u can avoid the metatables

wary radish
#

so ya, i've got like 4 levels of inheritance going on, more in some places

wary radish
sharp kiln
#

oh you actually use the other metatables

wary radish
#

i just use the nonstrict types for it since i cbf doing it accurately. i don't really care if the type is a perfect representation of the data, i just want the hints.

#

so i typecast

#

and just go well fuck it as long as the interface matches and the autocorrect hints are correct, i really couldn't care less

wary radish
#

but don't think that makes it any easier at 4 type refinement steps

#

specially when it branches in this very akward way where half the type is unknown at some stages, and the other half of the type is unknown at a different stage, then i have to somehow recursively make them all defined as the same type with different generics

#

so dont think it's any easier spanishkek

sharp kiln
#

yikes

wary radish
#

but i managed to do it

#

i have like 8 different refinements for my session class

#

shared->branch client OR branch server & gameshared (does not include client/server branch) -> gameserver (brings branched client/server and gameshared together in one type) -> client-only branch for remotesession/localsession (session belongs to localplayer).

wary radish
sharp kiln
#

looks like I got a lot to learn lol

wary radish
#

actually hold on i missed a sample

sharp kiln
wary radish
# sharp kiln why light mode tho

i like to be able to see what im doing. and i have a backlight so no eye strain. if you're sitting in pitch black room then yeah maybe use darkmode

#

game one

#

notice the scrollbar - someSharedCallback not shown in there

sharp kiln
#

yea

wary radish
#

and of course all other variations show correct intellisense suggestions

#

ya this also means the functions i can access depend on what level i pull the session from. it's a rather complex type so

#

anyway that's all, good luck with your types dogepopcorn

wary radish
#

my personal favorite part of this is no matter what level i pull the session from, the object always has its deepest refinement, e.g even though game functions not showing at earlier level, they do in fact exist, and any overrides the game made will be called. fun. and now i have to make more of these agony

sharp kiln
#

man types require a lot of work wow

wary radish
sharp kiln
#

hahhaa

wary radish
#

like its not even just the extra work

#

it's also technical debt

#

since you have to maintain the types if they ever change

sharp kiln
#

is it worth it though in the end?

sharp kiln
wary radish
#

coz i didn't even mention the other kind of table

#

setmetatable( A | B ... ) and making sure that's all refined correctly (that's how i do the localsession branch)

sharp kiln
#

il just focus on the basic for now

wary radish
#

this is the generic session, no distinction between local or remote fingerguns

#

ai suggesting to use localsessionfunc which doesnt exist in there lol

#

well -- depending on the session it might have it

wary radish
wary radish
sharp kiln
#

Ok so ive just looked at it and I understand why we use generics since when we didnt use generics if we called our base method it would expect the baseclass self not the childs self. However just in the generics part i just got some quick questions if thats ok.

1.Why isnt child_Index used? -- I assume its cause the methods exclusive to the child are stored in the metatable and the inherited methods are stored in the metatables metatable. So in Child_Meta

type child_Index = { dochildstuff:(child_Class)->() }
type child_Meta = typeof( setmetatable({}::{childFunc:(self:child_Class)->();}, {__index=my_Index<child_Data,child_Meta>}))
  1. I have to turn My_Meta from this:
type my_Meta<T={},C={}> = typeof(setmetatable(C, {__index:my_Index<T,C>}))

to this in order to fix the errors the type checker was giving me (asserted both types to an empty table since setmetatable expects values I assume)

type my_Meta<T={},C={}> = {typeof(setmetatable({} :: C,{} :: {__index:my_Index<T,C>}))}

3.I also had to change child_meta so it wouldnt error with the type checker from this:

type child_Meta = typeof( setmetatable({}::{childFunc:(self:child_Class)->();}, {__index=my_Index<child_Data,child_Meta>}))

to this also to fix the setmetatable issue and changing the = to :

type child_Meta = typeof( setmetatable({}::{childFunc:(self:child_Class)->();}, {} :: {__index: my_Index<child_Data,child_Meta>}))

Did I make any mistakes in these changes or is everything in order? thanks

forest spoke
#

i dont fw generics

#

so i dont use em

sharp kiln
# forest spoke i dont fw generics

yea I think u have to use them here tho cause the baseclass expects the baseclass self type but what if the child wants to the inherited methods then it wont work since it expects baseclass type so u have to use generics im just a bit confused with some parts of the code

wary radish
# sharp kiln Ok so ive just looked at it and I understand why we use generics since when we d...
  1. probably a mistake on my part, i didn't have my reference open. but it's close. there's another step when defining the implementation for these that i didn't show, it is a rather difficult type to work with hence don't bother.
  2. you changed the object type into an array
  3. your correction looks about right. i use the new typesolver so metatables are slightly easier not needing the typeof call. type thing_C = setmetatable<properties,recursive_metatable_chain>
wary radish
#

technical debt and extra work considering

#

it's nice to know the full pattern, but you probably don't need it half the time. same reason for using local t={} t.__index=t i suppose; convenience be convenient.

sharp kiln
# wary radish 1. probably a mistake on my part, i didn't have my reference open. but it's clos...

1.ok so I should use child_index to cast the type to the metatable and if theres any metatable specific methods I can just use intersection and combine it with the child_index methods
2.Yea i have to do that for some reason since the type checker says I cant type cast my_Meta to {} for some reason saying they are not related

type my_Meta<T={},C={}> = typeof(setmetatable({} :: C,{} :: {__index:my_Index<T,C>}))
type my_Class<T={},C={}> = typeof(setmetatable({}::my_Data & T, {}::my_Meta<T,C>))
#

Its probs cause im type casting C to {} maybe?

wary radish
#

this is a trick to make the recursive metatable work