#Instantiate only inhabitant of a unit type

90 messages · Page 1 of 1 (latest)

muted kettle
#

Is it possible to instantiate the only inhabitant of a unit type given the unit type as a generic parameter?

type Unit = [1, 2, 3]

function f<T>(): T { ... }

const a: Unit = f<Unit>()
rough drum
#

No, types don't exist at runtime.

muted kettle
#

The type shouldn't be needed at runtime since the type is known at compile time

#

Is there a stage in the ts compiler where the type of the generic is known for a function such that a transformer can be written to instantiate the unit type

dense dome
#

however i wouldn't usually reach for a transformer as my first instinct. in TS it's better to derive types from values rather than the other way around, for example:

inland parcelBOT
#
const unit = [1, 2, 3] as const
type Unit = typeof unit
//   ^? - type Unit = readonly [1, 2, 3]

const a: Unit = unit
dense dome
#

(that may or may not apply to your use case. if you can share more details maybe i'll have other ideas)

#

also you should be aware that this isn't really a unit type. it's actually hard to make a real unit type in TypeScript because everything can be subtyped (null, undefined, and unique symbol are probably as close as you can get). also since [1, 2, 3] is not a primitive there can be many different values of that type and they're distinguishable by reference equality, for example:

inland parcelBOT
#
mkantor#0

Preview:```ts
type Unit = [1, 2, 3]

const a: Unit = [1, 2, 3]
const b: Unit = [1, 2, 3]
console.log(a === b) // false (they're different references)

const withExtraProperty = <T extends {}>(
x: T
): T & {extraProperty: true} => {
return {...x, extraProperty: true}
}
const c: Unit = withExtraProperty(b) // c is still of type Unit
console.log(c)```

muted kettle
proud canyon
#

yes, the problem is that types simply do not exist in TS

#

in contrast to other languages

#

that said, it's not like most other languages even have unit types in the first place

#

perhaps it's possible in haskell and/or scala. definitely not most other languages though

dense dome
#

i'm not even sure about haskell. i know it has () as a unit type, but that might be the only one? same deal in rust

muted kettle
#

in rust you can define your own unit type

#

haskell too

proud canyon
muted kettle
#

is there any way to derive a hash from a generic type?

#

or is this just not possible in ts

dense dome
dense dome
#

@muted kettle what's the bigger picture here? in your real code what will t do/be used for?

muted kettle
#

ecs in the style of bevy where the query type is where you specify what you want

#

since typescript has typed tuples which you can use as an hlist I thought it would viable

dense dome
#

ah yeah, you won't be able to have exactly that same API because TS doesn't do type-directed emit. however you can use a value as your key into the registry (not sure if that's the correct terminology for ECS but hopefully you know what i mean)

#

my first instinct would be to have a unique symbol for each kind of thing you want to look up and use that

proud canyon
#

you could also just use classes instead

dense dome
#

yeah or use the class value to look up instances of the class

muted kettle
#

a static member with Symbol() as value?

proud canyon
#

no, a unique symbol associated with a type

#
declare const TYPE_ID: unique symbol;
interface TypeId<T> {
  [TYPE_ID]: T;
}

function makeTypeId<T>(name?: string): TypeId<T> {
  return Symbol(name) as TypeId<T>;
}
#

idk

#

oh wait

#

you still need a way to filter instances

#

yeah a static member would work

#

a WeakMap<unknown, Set<symbol>> would work too

#

where the key is the object and the value is the set of components

#

wait no

#

🤷 idk how ecs works

dense dome
#

i was thinking something like this (note i have never done ECS so i could be making faulty assumptions):

inland parcelBOT
#
mkantor#0

Preview:```ts
class Component {
baseComponentStuff = true
}

class Foo extends Component {
static id = Symbol("foo")
fooStuff = "whatever"
}

class Bar extends Component {
static id = Symbol("bar")
barStuff = 42
}

const registry = {
[Foo.id]: new Foo(
...```

muted kettle
#

a static id for each class would work if I can get an array from them from a generic array/typed tuple

proud canyon
#

!hb mapped types

inland parcelBOT
proud canyon
#

this works for mapping arrays

muted kettle
rough drum
#

You can just use a Map and the component class itself as the key for the registry

#

That removes the need for a unique id per component.

proud canyon
proud canyon
muted kettle
#

You'd iterate over each entity in the naive implementation in each tick instead of only the entities with the required components which could be orders of magnitude slower

dense dome
# inland parcel

this would be a better type for lookup:

<ID extends keyof Registry>(id: ID, registry: Registry) => Registry[ID] | undefined
proud canyon
#

in that case WeakMap<unknown, symbol>

#

where the key is the component and the value is the type

#

like

#

on one hand a static id works

#

on the other hand that's just the same as using classes

#

if you're concerned about structural typing making typescript confused between class C1 {} and class C2 {} (both have the same shape) you can work around that by introducing a private member declare #_: unknown; to make the classes nominal instead of structural

dense dome
#

yeah i mostly agree. if you really only ever need to store class instances (and you can guarantee that your classes have a 1:1 mapping of constructor => instance and that the constructors are structurally-distinct from one another), then i'd just use the constructor itself as the ID

rough drum
#

Speaking of performance, a big part of what makes ECS very performant, is that components are flat arrays in memory so systems can process them extremely quickly due to cache locality.
In JS, if your components are objects, welp they are all over the heap already.

muted kettle
#

I think I'll try to just manually supply the "not so unit-type" unit-type

proud canyon
rough drum
#

What does the non naive implementation look like?

proud canyon
#

actually

muted kettle
#

archetypes/tables

#

I care more about the ergonomics of using ecs than the performance

muted kettle
rough drum
#

Well, instead of query<Position, Velocity>() you could just do query(Position, Velocity).

proud canyon
rough drum
#

That way at runtime you still have values to perform the lookup.

proud canyon
#

you have to know the type to get the components in the first place, right?

muted kettle
rough drum
#

How so?

muted kettle
#

unless typescript can differentiate between two spread operators on arrays as function parameters which extend different classes

rough drum
#

Two spread operators?

muted kettle
#

I guess I'll try something like query<T extends unknown[], F extends Filter<unknown>[]>(t: T, f: F)

inland parcelBOT
proud canyon
#

alternatively

#

(really janky)

#

!ts

inland parcelBOT
#
class Component {
    baseComponentStuff = true
}

function component<T>() {
    const instances: T[] = [];
    return class extends Component {
        static instances = instances;

        constructor() {
            super();
            instances.push(this as any);
        }
    } as typeof Component & { instances: T[]; }
}

class Foo extends component<Foo>() {
    fooStuff = 'whatever'
}

class Bar extends component<Foo>() {
    barStuff = 42
}

new Foo();
new Bar();
console.log(Foo.instances, Bar.instances);```
rough drum
muted kettle
rough drum
#

Or do an option object:

world.query({
    contains: [Foo, Bar, Baz],
    filters: [Qux],
})