https://github.com/mrdoob/three.js/blob/dev/src/objects/Mesh.d.ts#L16
The type of property material on the Mesh class is Material | Material[].
I run into an issue when i use:
const myMesh = new Mesh( someGeometry, new MeshBasicMaterial())
myMesh.material.map = foo //error
The workaround for me is:
const someMaterial = new MeshBasicMaterial()
const myMesh = new Mesh(someGeometry, someMaterial)
someMaterial.map = foo // no error, because the interface is from MeshBasicMaterial
However, the code can end up being somewhat ugly. I tend to end up with something like:
this.material // :Material
this._typedMaterial // ShaderMaterial
Prettier goes crazy when i try to cast it and i end up with something like:
;(this.material as MeshBasicMaterial).map = foo
If i have a few lines to set, i need to do this on each, hence storing it in a variable with the proper type feels less awkward.
Even something that is generic and common for all Material sub classes fails because of the | Material[] such as dephTest.
I was wondering if something involving generics could be applied here, ie:
const myMesh = new Mesh<MeshBasicMaterial>(someGeometry, new MeshBasicMaterial)
myMesh.map = foo //no error because Mesh would use <T>
I'm not sure what the exact syntax for this could be, and if <T> can be limited to a subclass of Material, but the Material interface seems somewhat awkward since it's sort of a utility class, everything we end up using extends from it. But even the class being generic is not that much of an issue, the possibility of the type being an array breaks it more.
Is this an issue, and if it is, what would be the solution? Is there a more elegant typescript approach where the property could be just inferred from the type of material passed?
Is there a more elegant typescript approach where the property could be just inferred from the type of material passed?
I'm not aware of anything like that, except generics.
I think you're on the right track with generics, see https://github.com/mrdoob/three.js/issues/19072 and https://github.com/mrdoob/three.js/pull/18720. Probably this can be solved with some changes to the type definitions, although they're somewhat complex changes so I'd hope we can get some more experienced TypeScript users to take that on. That .material might be an array seems like the trickiest part.
I just did something in the TS playground regarding this more elegant approach. I don't quite understand it but i went by this SO post.
class Geometry {
foo(){}
}
class BufferGeometry {
bar() { }
}
class Material {
foo() { }
}
class BasicMaterial extends Material {
bar (){}
}
class PhongMaterial extends Material {
baz(){}
}
class Mesh {
constructor(
public geometry: Geometry | BufferGeometry,
public material : Material | Material[]
){}
}
const myMesh = new Mesh(new BufferGeometry(), new BasicMaterial())
myMesh.material.bar() // error, bar() doesn't exist on Material | Material[]
class MeshGenerics <G,M>{
constructor(
public geometry: G,
public material : M
){}
}
const myMeshG = new MeshGenerics<BufferGeometry, BasicMaterial>(new BufferGeometry(), new BasicMaterial())
myMeshG.material.bar() //no error, but i have to repeat both arguments twice
const myMeshG2 = new MeshGenerics(new BufferGeometry(), new BasicMaterial())
myMeshG2.material.bar() //this magically works?
If G can be made to be either Geometry | BufferGeometry seems like it could work.
@donmccurdy
Heh, this seems like it works :)
Ill make a PR we can discuss there
class MeshGenerics <G extends Geometry | BufferGeometry,M extends Material | Material[]>{
constructor(
public geometry: G,
public material: M
){}
}
const myMeshG = new MeshGenerics<BufferGeometry, BasicMaterial>(new BufferGeometry(), new BasicMaterial())
myMeshG.material.bar()
const myMeshG2 = new MeshGenerics(new BufferGeometry(), new BasicMaterial())
myMeshG2.material.bar()
const myMeshG3 = new MeshGenerics(new Geometry(), new PhongMaterial())
myMeshG3.geometry.foo()
myMeshG3.material.foo() //from Material
myMeshG3.material.baz() //from PhongMaterial
myMeshG3.material.bar() ///error :)
Might be simpler with a Type Alias? Like:
type AnyGeometry = Geometry | BufferGeometry;
class Mesh <G extends AnyGeometry, M extends Material | Material[]> {
constructor (geometry: G, material: M) {
// ...
}
}
I'm not quite sure how the Material[] portion of that generic will behave, but if the user has to do some explicit casting for elements of a multi-material array that seems reasonable.
Looking into how to test this, i've made a change and linked this branch of three into my typescript background. Would AnyGeometry be a thing thats private to say Mesh or a more generic building block known to the rest of the system?
I don't think the AnyGeometry type would need to be exported or used anywhere else. Maybe it's useful to do so, but it doesn't seem necessary. There may be some cases where the user (e.g. the dev writing TS code with three.js) needs to cast...
const geometry = mesh.geometry as BufferGeometry;
... because I doubt the TS compiler will understand .isBufferGeometry checks. But that casting doesn't require access to the AnyGeometry alias.

It seems like it's an improvement. It somehow figured out that this array of phong and basic, can have either phong or basic. Can't figure out which one is which, but it is aware that the array can only contain two types if i've filled it with two types. Yeey! This is a huge TS milestone for me.
... because I doubt the TS compiler will understand .isBufferGeometry checks.
This on the other hand, i know doesn't play super well with TS? Is it affected by this? I think that check could always be hacked by (geometry as any).isBufferGeometry. I think with TS it'd make more sense to have a nasty, but known issue under the hood, but then have clear and safe types facing the user.
^Yes, I think that's fine. If the user is strict with their own types (which is the whole point of TS) the compiler will know that mesh.geometry is a BufferGeometry and the code will stay clean. It really only becomes an issue for developers writing abstractions that must accept anything, in which case the required checks just become uglier, as APIs that deal with loose input in TS generally do. Because Geometry is deprecated, that problem will go away.
Seems to be a duplicate of #19072 then?
Yes, this is the crux:
In my current project, I need to change the Material property dynamically and I always need to keep an external reference on the Material class used to avoid casting.
Most helpful comment
I'm not aware of anything like that, except generics.
I think you're on the right track with generics, see https://github.com/mrdoob/three.js/issues/19072 and https://github.com/mrdoob/three.js/pull/18720. Probably this can be solved with some changes to the type definitions, although they're somewhat complex changes so I'd hope we can get some more experienced TypeScript users to take that on. That
.materialmight be an array seems like the trickiest part.