luau-lang / luau

A fast, small, safe, gradually typed embeddable scripting language derived from Lua
https://luau.org
MIT License
3.98k stars 373 forks source link

Difference types #1253

Closed DimitarBogdanov closed 4 months ago

DimitarBogdanov commented 4 months ago

Problem

Consider this:

type X = { a: number }
type Y = { b: number }
type Z = { c: number }

type M = { a: number, b: number, c: number }

It's possible to create a type like M without writing the property names individually.

type XYZ = X & Y & Z

But it's not possible to create a type like M which excludes properties from another type.

type N = M / X

Use case

You have an Instance which has many objects in it. You want to use the Instance as a dictionary, where you can use e.g. part.Decal and use the child decal. This is certainly doable!

--!strict
local holder = workspace.Part

type Holder = typeof(holder) --> Part & <all children instances of the workspace part>

Now you want to make a function which returns the Holder type.

export type Holder = typeof(holder)
function getHolder() --[[ ... ]] end

But you don't want your function's consumer to have access to irrelevant methods such as GetChildren and Name on the Holder type. This is where a difference of sets comes in place (coloured area): image

So we want to get everything from Holder and remove everything from Part on it. This could look something like this

type Holder = typeof(holder) / Part

And boom! Now we can return Holder and we won't see anything from Part. P.S. I use / instead of - as the suggested syntax, because a minus looks awkward to me, but it's definitely not the only possible way!

goldenstein64 commented 4 months ago

There is a draft RFC for negation types: https://github.com/luau-lang/rfcs/pull/29. Typically, this would mean you could write your example as Holder & ~Part, but it places restrictions on structural types, which is your primary concern.

So the operator you're proposing here would cover a different use case, I just wanted to point out that if the negation type RFC was able to test negated structural types, it would cover your use case as well.

Also, it doesn't look like there are any type operations that could be mistaken for value operations, and I think that's because of casting; it happens inside expressions that execute at runtime.

-- what could this mean?
local a = b :: T / U

-- it could be one of:
local a = (b :: T) / U
local a = b :: (T / U)

Perhaps you could use the symbol set theory/Wikipedia uses, the backslash \ character.

alexmccord commented 4 months ago

This design looks a lot like omit<T, keyof<U>> so this is really asking for an omit type family.

And yeah, the design conflicts with local a = b :: T / U as above.

DimitarBogdanov commented 4 months ago

Also, it doesn't look like there are any type operations that could be mistaken for value operations, and I think that's because of casting; it happens inside expressions that execute at runtime.

I did not consider the fact that types appear in expressions, you're correct. I agree with the backslash idea, actually like it a bit more.

This design looks a lot like omit<T, keyof> so this is really asking for an omit type family.

100% plausible, I'm not very well vested in TS.

alexmccord commented 4 months ago

Going to close this. https://github.com/luau-lang/rfcs will be the right place to submit such a feature request.