Pixi.js: Help Wanted: TypeScript Conversion Update

Created on 1 Feb 2020  ·  24Comments  ·  Source: pixijs/pixi.js

Hello PixiJS Community,

We have reach a major milestone in converting PixiJS to TypeScript by converting core and all it dependencies. Now that we are over this hump, we can start on the rest of the packages that dependency on this small set of core packages.

We could definitely use a hand from devs to convert the rest of these packages. If you are interested in converting a package, please let me know which one. There are a few guidelines for converting packages, which you can see from the existing completed PRs.

Converting Gotchyas

  • We are trying to maintain the JSDocs until everything is completed, and then convert to Typedoc and emit types afterwards. We ask that you maintain JSDocs and make sure that they still build and show up correctly via npm run docs
  • Please use git mv to rename JS files to TS, or else we lose the Git history. Some Git GUIs may not pick up these changes.
  • Please do not add public access modifiers to internal-only methods or members, leave access undefined in these cases.

Packages

To Claim a package, please create Draft PR for it

  • [x] @pixi/accessibility #6379
  • [x] @pixi/app #6376
  • [x] @pixi/constants #6173
  • [x] @pixi/core #6340, #6373
  • [x] @pixi/display #6261, #6339, #6349, #6371
  • [x] @pixi/extract #6381
  • [x] @pixi/graphics #6352
  • [x] @pixi/interaction #6656
  • [x] @pixi/loaders #6385
  • [x] @pixi/math #6141
  • [x] @pixi/mesh-extras #6396
  • [x] @pixi/mesh #6382
  • [x] @pixi/mixin-cache-as-bitmap #6630
  • [x] @pixi/mixin-get-child-by-name #6621
  • [x] @pixi/mixin-get-global-position #6637
  • [x] @pixi/particles #6449
  • [x] @pixi/polyfill #6654, #6669
  • [x] @pixi/prepare #6481
  • [x] @pixi/runner #6164
  • [x] @pixi/settings #6315
  • [x] @pixi/sprite-animated #6397
  • [x] @pixi/sprite-tiling#6398
  • [x] @pixi/sprite #6375
  • [x] @pixi/spritesheet #6389
  • [x] @pixi/text-bitmap #6479
  • [x] @pixi/text #6390
  • [x] @pixi/ticker #6186
  • [x] @pixi/unsafe-eval #6655
  • [x] @pixi/utils #6262
  • [x] @pixi/canvas-display #6659
  • [x] @pixi/canvas-extract #6503
  • [x] @pixi/canvas-graphics #6663
  • [x] @pixi/canvas-mesh #6664
  • [x] @pixi/canvas-particles #6622
  • [x] @pixi/canvas-prepare #6657
  • [x] @pixi/canvas-renderer #6499
  • [x] @pixi/canvas-sprite-tiling #6665
  • [x] @pixi/canvas-sprite #6658
  • [x] @pixi/canvas-text #6666
  • [x] @pixi/filter-alpha #6383
  • [x] @pixi/filter-blur #6383
  • [x] @pixi/filter-color-matrix #6383
  • [x] @pixi/filter-displacement #6383
  • [x] @pixi/filter-fxaa #6383
  • [x] @pixi/filter-noise #6383

Bundles

  • [ ] pixi.js-legacy #6673 In Progress @bigtimebuddy
  • [ ] pixi.js #6673 In Progress @bigtimebuddy

Most helpful comment

I hate making API changes to accommodate typing limitations, just doesn’t feel right to me. Personally, I’d rather use any then create a new method. Having such a large API surface is already a burden.

All 24 comments

More tips:

  • Try to put all extra import's (actually its import type) separate from existing imports. We'll add import type when new TS version rolls out. However linter will stop you from using two lines of import from same module, its fine to mix them in that case.

  • You may use both Array<X> and X[] notations, I usually use Array<X> for structures that are pushed-popped often (aka Lists). X[] for things that have fixed small length or if size is decided at the same place they are constructed (regular array).

  • If readonly field is changed only in destroy() - you can use any conversion there. If its used somewhere else - remove readonly. We can decide whether to make it a private field + readonly property later.

I could do @pixi-text this weekend unless someone beats me to it.

note: @pixi-tiling will have PIXI.TilingSprite.from which night not possible to do in TS. We can just deprecate it.

Go for it, @qtiki. I'll be online in weekend in case you find strange things.

Ok, @qtiki, your choice will reserved after sending draft PR.
Thanks!

Agreed. Draft PR is the best way to claim a package to convert. Thanks for helping

Ok, @qtiki, your choice will reserved after sending draft PR.
Thanks!

I opened draft PR #6390. I just renamed the .js files to .ts to have some changes to be able to create the PR. I'll try and do the actual converting this weekend.

Some generic things I've run into while converting the Text package to TypeScript:

So I ran into a fundamental problem with the TypeScript properties. It seems that TypeScript does not support different types for property getter and setter: https://github.com/microsoft/TypeScript/issues/2521

There are some places in the Text and TextStyle classes where this would be a huge benefit. For example fillStyle and stroke in TextStyle accept a number that is then converted to a string, so the getter shouldn't return an union type with a number. Now that it returns an union type with a number I have to cast it to be able to pass it to CanvasRenderingContext2D fillStyle.

Also the Text class itself accept an object with the text style, which is then passed to new TextStyle - or an instance of TextStyle directly. Similarly in here the getter will always return a TextStyle instance but has to be typed with the same union type as the setter. So this will cause some unnecessary type checking (or casts) in userland to be able to call methods of TextStyle.

I don't really know what would be the best solution for this in the long term, but for now I guess that will just have to do.

One other thing I noticed is that there is quite a bit of code that re-uses local variables with different type, which does not play really well with TypeScript. I didn't want to touch the existing code too much so I just used union types and some ugly casting here and there to get it to compile. I guess those sort of things can be improved over time with the boy scout principle.

@qtiki
Why you write problems in this topic instead your PR?

@qtiki
Why you write problems in this topic instead your PR?

I believe these are generic issues that people will run into elsewhere with the TypeScript conversion and not just text package specific problems. The property issue for one is something that is a fundamental design question for the future if they are to be typed correctly.

I've been studying the problem with TypeScript setters and getters not being able to use different types ever since I ran into it. It seems that the problem is a very complex one and it might never get "fixed". So here's my suggestion for a general guideline in converting these types of properties to TypeScript:

This is a simplified example of what we want: a property that accepts string | number but is stored internally (and returned) as string. Naturally we could return the property as string | number but that would mean that the users of the API would be faced with completely unnecessary typechecks.

class Foo {
    constructor() {
        this._bar = '';
    }

    // error: 'get' and 'set' accessor must have the same type.(2380)
    get bar(): string {
        return this._bar;
    }

    // error: 'get' and 'set' accessor must have the same type.(2380)
    set bar(value: string | number) {
        this._bar = value.toString();
    }

    private _bar: string;
}

Here's how we can work around this:

class Foo {
    constructor() {
        this._bar = '';
    }

    // Return the strictest type possible in the getter.
    get bar(): string {
        return this._bar;
    }

    // Use the same strict type for the setter as the getter.
    set bar(value: string) {
        // Call the conversion function.
        this.setBar(value);
    }

    // Implement a separate conversion function that accepts all supported types.
    public setBar(value: number | string) {
        this._bar = value.toString();
    }

    private _bar: string;
}

This way the user gets the correct type when reading the property. TypeScript users who want to assign a number to bar will need to call the setBar conversion function. However this would not be a breaking change for existing JavaScript users since the property setter calls the conversion function - which means the untyped property setter does actually accept numbers.

What do you guys think, does this make sense? I'm not sure how often this type of pattern is used but at least in the text package I ran into it more than once.

I hate making API changes to accommodate typing limitations, just doesn’t feel right to me. Personally, I’d rather use any then create a new method. Having such a large API surface is already a burden.

I never will approve hacks with set* method, agree with @bigtimebuddy that any type is more friendly that set* method.

In the current case, it is quite acceptable return (string | number) type for getter.

Actually, sometimes setBar is a good idea because of inheritance issues of set, but needs more symbols in prefx, like _$setBar() :) Not in this case though.

@qtiki thank you for highlighting the problem, I encountered it many times when I made pixijs ts fork for my job projects.

Yes, Text is pain. Yes, we have to think about it more.

I agree that the setBar is not the prettiest solution. To be honest I was quite surprised that TypeScript doesn't have support for these sort of setters with type-coercion. Just a word of warning though what it means to use looser typing or even any for these properties in regards to TypeScript's flow control:

class Foo {
    constructor() {
        this._bar = '';
    }

    get bar(): string | number {
        return this._bar;
    }

    set bar(value: string | number) {
        this._bar = value.toString();
    }

    private _bar: string;
}

const foo = new Foo();

// TypeScript's flow control will assume from now on that `bar` is a `number`
foo.bar = 42;

function add(a: number, b: number) {
    return a + b;
}

// Call to `add` shouldn't be allowed since the value in `bar` is actually a string
// This will print `4242` instead of `84`
console.log(add(foo.bar, 42));

I think in the long run the cleanest option would be to go with something like this:

class Bar {
    constructor() {
        this._bar = '';
    }

    get(): string {
        return this._bar;
    }

    set(value: string | number) {
        this._bar = value.toString();
    }

    private _bar: string;
}

class Foo {
    constructor() {
        this.bar = new Bar();
    }

    public bar: Bar;
}

const foo = new Foo();
foo.bar.set(42);
console.log(foo.bar.get());

This type of set is not without a precedent as it is exactly how PIXI.Point is implemented. Basically all user-facing api would probably accept the Bar instance "as is" without having to call the get everywhere manually.

Naturally this would be a breaking change so not something for the near future, I just wanted to put my two cents in.

And I just realized that my previous suggestion would no longer be a "real" property so things like Object.assign would no longer work with the setter. So maybe not the greatest idea after all.

Hey there, I'm a long-time PixiJS user and user of Typescript. I'd be able to contribute on this - It looks as though most of the packages have been done at this point. Apart from the canvas ones. Are there any I can help out on?

@lloydevans yes!!! Fancy doing prepare or text-bitmap? Both are good hopefully not super complicated packages.

@bigtimebuddy Great! I could have a look at doing text-bitmap.

Sounds good! Please make a draft PR once you get started so we can track it here.

Ok, will do. I'll have a look at some of the existing conversions and PRs now and will follow suit.

It seems that the individual packages in npm still don't have types. I'm guessing that's something that can be addressed after this issue is done? Would be great to be able to use them to reduce build sizes without sacrificing type safety.

Home stretch everyone! Just a couple of packages left!

Huge thank you to @Zyie, @ivanpopelyshev, @SerG-Y, @eXponenta and all the other contributors who helped make this migration possible.

Was this page helpful?
0 / 5 - 0 ratings