Assuming you know of the __index and __newindex metamethods, you can use it to return the instance instead of a table.

local realWorkspace = workspace
local workspace = {}
workspace.__index = realWorkspace
function workspace:PrintHi()
print('hi')
end
function workspace.new()
local newWorkspace = {}
-- inside the newWorkspace table, you add your properties and events, not functions
return setmetatable(newWorkspace, workspace)
end

Everything is explained well in the tutorial so you should check that out. You’ll notice you won’t be able to call native workspace methods natively so you can use this:

--//Variables
local realWorkspace = workspace
local functions = {}
functions.__index = realWorkspace
--//Functions
function functions:GetItems(class:string)
local items = {}
for _,Item in pairs(game.Workspace:GetDescendants()) do
if Item:IsA(class) then
table.insert(items,Item)
end
end
return items
end
function functions.new()
local newFunctions = {}
return setmetatable(newFunctions, functions)
end

--//Variables
local realWorkspace = workspace
local functions = {}
functions.__index = realWorkspace
--//Functions
function functions:GetItems(class:string)
local items = {}
for _,Item in pairs(game.Workspace:GetDescendants()) do
if Item:IsA(class) then
table.insert(items,Item)
end
end
return items
end
function functions.new()
local newFunctions = {}
return setmetatable(newFunctions, functions)
end
--//logic
local result = functions.new()
result:GetItems("BasePart")

Oh I see the issue, you will have to make the __index metamethod a function that returns a function inside of functions if it exists, else returns the real instance’s function. So like

local functions = {}
-- class methods go in the functions table
function functions:__index(key)
if functions[key] then
return functions[key]
else
return workspace[key]
end
end
function functions.new()
local object = {}
-- properties and events here
return setmetatable(object, functions)
end

--//Variables
local realWorkspace = workspace
local functions = {}
functions.__index = realWorkspace
--//Functions
function functions:GetItems(class:string)
if functions[class] then
return functions[class]
else
return workspace[class]
end
end
function functions.new()
local object = {}
return setmetatable(object, functions)
end
--//logic
local result = functions.new()
result:GetItems("BasePart")

--//Variables
local realWorkspace = workspace
local functions = {}
functions.__index = functions
--//Functions
function functions:GetItems(class:string)
if functions[class] then
return functions[class]
else
return workspace[class]
end
end
function functions.new()
local object = {}
return setmetatable(object, functions)
end
--//logic
local result = functions.new()
print(table.concat(result:GetItems("BasePart"),","))

it doesnt print

nor does this one -

--//Variables
local realWorkspace = workspace
local functions = {}
functions.__index = functions
--//Functions
function functions:GetItems(class:string)
local items = {}
for _,Item in pairs(game.Workspace:GetDescendants()) do
if Item:IsA(class) then
table.insert(items,Item)
end
end
return items
end
function functions.new()
local object = {}
return setmetatable(object, functions)
end
--//logic
local result = functions.new()
print(table.concat(result:GetItems("BasePart"),","))

This still is not a function. It has to be a function that checks if functions[index] exists where index is the second argument passed to the __index function. If it does exist, return functions[index], else return workspace[index]

Yeah, object wrappers are difficult to understand but one day it just clicked for me so I’ll try explaining it as best as I can. So, __index is a metamethod that can either be a function or another object.

If it is a function it will call that function, if it is another object, it will look in this other object for the index

1- newObject = object.new() - newObject is a table that contains properties/events, and whose metatable has a __index metamethod that is a function.
2- newObject.nonexistentkey - Since no entry in newObject exists with the index nonexistentkey, it gets the __index metamethod of the newObject object, and calls that __index metamethod if object.nonexistentkey does not exist. In our case it does not exist, so it passes the table that was attempted to be indexed as the first argument (in our case it’s newObject) and as the second argument it’s the key that was used to attempt to index newObject. We do not need to call __index with parentheses because indexing newObject with a nonexistent key calls this already.
3- In our __index function, we attempt to index functions, which is the table with class methods that extend the actual workspace methods, with nonexistentkey. This does not invoke any metamethods. If it exists return it.
4- nonexistentkey does not exist in the functions table, so we index the real instance with nonexistentkey, this will either error properly, or returns the real instance property.

In the end, imagine it like this,
This line: local myProperty = newObject.nonexistentkey
Is the same same as doing

local myProperty = getmetatable(newObject).__index(newObject, 'nonexistentkey')

So, in the end, the process is like this, where you attempt each step and if one of the steps is successful, use the result from the successful attempt, if it errors, do nothing, if nil is returned but it did not error, proceed:
1- look in newObject for nonexistentkey
2- look in newObject’s metatable for nonexistentkey
3- look in realWorkspace for nonexistentkey
4- no options left, return nil.

Put the metamethods in your functions table, then use the functions table as your metatable.

function functions:__index(key)
if functions[key] then
return functions[key]
else
return workspace[key]
end
end
--...
return setmetatable(newTable, functions)