Я участвовал в Ludum Dare 40 с группой друзей, большинство из которых не были программистами. Конечный результат нашей ужасной игры - здесь (исходный код). Я думал о нескольких языках для сборки, но остановился на браузерной игре для простоты использования (никому не нужно специальное устройство). Я хотел попробовать что-нибудь другое, кроме Dart, над которым я сейчас работаю в Google.

TypeScript сам по себе - это просто язык и комбинация компилятора и средства проверки типов, и не дает особых мнений. Итак, я использовал этот стек:

  • NPM 5.5.1 и Node v8.9.1
  • TypeScript 2.6
  • TSLint (сторонний линтер от Palantir)
  • Prettier (форматер кода)
  • WebPack (сборщик модулей, система сборки и сервер разработки)
  • VSCode (IDE) с плагинами TypeScript и TSLint
  • Phaser (игровой движок)
  • Lodash (этакое расширение стандартной библиотеки для JS / TS)

Мы пропустили какое-либо тестирование (пробовали использовать Jest, но не вышло).

В целом, мы все чувствовали себя продуктивными и могли выполнять свои задачи и находить разумные обходные пути, когда то, что мы хотели, не работало. Однако между моим опытом работы с Typescript и другими языками было несколько примечательных моментов.

Хороший

  • «Золотой» путь инструментов был фантастическим. Когда у TypeScript и VSCode не было никаких проблем с конфигурацией, среда IDE была на высшем уровне, казалось, что вы используете Visual Studio и C #. Мгновенный анализ (я имею в виду мгновенный, я ни разу не видел слова «анализирующий»). Хотите добавить пакет? «npm install - save-dev ‹package›», и тогда у вас будет поддержка автозаполнения мгновенно, не дожидаясь индексации или анализа. Волшебный.
  • Система статических типов поверх JavaScript работает намного лучше, чем я, вероятно, признал несколько лет назад. У меня есть несколько сомнений по поводу специфического (преднамеренного) выбора системы типов TypeScript (см. «Ну» и «плохо» ниже), но это может быть либо просто личное мнение, либо я ошибаюсь .
  • По крайней мере в одном месте мне нужно было использовать библиотеку dungeon-factory (которая, вау, возникла из Rogue-подобной игры Боба, написанной на Dart), но она не была написана на TypeScript, и я хотел типизированный API. Я смог объявить один в своем собственном проекте и использовать его, как если бы это были типы библиотеки (см. Dungeon-factory.d.ts):
  export function generate(options: {
    height: number;
    width: number;
  }): IDungeon;

  interface IDungeon {
    readonly rooms: IRoom[];
    readonly tiles: ITile[][];
  }

  interface IRoom {
    readonly x: number;
    readonly y: number;
    readonly width: number;
    readonly height: number;
  }
  • Мне понравилась эргономика ключевого слова readonly, вроде «final», но его можно было инициализировать в теле конструктора. Мне кажется, что иногда мне приходится делать все возможное, чтобы получить в Dart переменные типа final, без использования конструкторов фабрики:
class Grid {
  public readonly cells: Cell[];
  public readonly collisions: number[][]; // Need for pathfinding.
  public readonly w: number;
  public readonly h: number;

  constructor(...) {
    this.w = '...';
    ...
  }
}
  • Кроме того, мне не нужно объявлять переменные «дважды»:
class HudList {
  constructor(
    private readonly game: Game,
    private readonly x: number,
    private readonly y: number
  ) {}
}
  • Отсутствие обнуления по умолчанию отлично (по большей части). Мы обращаемся к NPE (исключения нулевого указателя или «undefined не является функцией») в программе только тогда, когда нам приходилось обходить проверки допустимости пустых значений (обычно для использования других библиотек или для работы с типами объединения). Например, эти утверждения являются ошибками:
let x; // Use const instead, this is never reassigned.

function foo(x?: number) {
  // Type 'number | undefined' is not assignable to type 'number'.
  let y: number = x;
}
  • Сервер разработки веб-пакетов (который обслуживает связанные JS / CSS / HTML / Assets) был молниеносно быстрым, хорошо документированным и простым в использовании. Я не думаю, что мне когда-либо приходилось перезапускать сервер один раз за 48 часов разработки кроме, когда я менял конфигурацию веб-пакета. Возможно, там даже есть плагин для автоматического перезапуска, когда это произойдет, но я не уверен.
  • Типы справа делают так гораздо более понятным в системе типов с выводом типов. Это действительно побуждает вас никогда не писать тип unles, который вы делаете частью API, т.е. «эта строка должна завершиться ошибкой, если она не присвоена номеру». или вы пытаетесь унизить:
let x = 5;

let max: number;
while (true) {
  ... 
  max = current;
}
  • Мне понравилось (очень) наличие «общедоступного», «частного» и «защищенного», а не только соглашение Дарта об использовании префикса подчеркивания «_» и некоторые подсказки анализа, не встроенные в основной язык. Это сделало для всех нас намного яснее, что следует / не следует использовать. По сравнению с Dart мне больше всего нравилось то, что private был class-private, а не file-private. С другой стороны, TS не имеет «внутренней» опции, но, возможно, это и хорошо.

В целом, мы все чувствовали себя значительно более продуктивными, используя TypeScript и другие инструменты (webpack, tslint и т. Д.), Чем я когда-либо использовал простой JavaScript и, скажем, Grunt или Bower.

Мех

  • Использование чистых JS-библиотек из TypeScript было немного неудачным. Например, и Lodash, и Phaser находятся в вариантах JavaScript / JavaScript (т.е.используют Babel для переноса новых функций JS не во всех браузерах). Возможно, я «ошибался», но я сделал это хотя бы раз и не знаю, зачем мне:
game.cache.addTilemap('dynamic', null as any, csv, Phaser.Tilemap.CSV);
  • Мне очень не хватало названных аргументов, исходящих от Дарта. Вы можете как бы выразить то же самое, используя анонимный объект, но у него есть раздражающие аннотации на уровне языка, которые требуют библиотек для сортировки патчей. Например:
// My beautiful API.
function goblin(armory: Armory): RenderTexture {
  return armory.peonTexture({
    skin: SkinColor.Green,
    shirt: {
      color: ShirtColor.Green,
      style: 9,
    },
    pants: PantsColor.Green,
    shield: ShieldColor.OrangeGreen,
  });
}

// My less beautiful implementation.
public peonTexture(options?: {
    skin?: SkinColor;
    hair?: HairColor | { color: HairColor; style: number } | null;
    beard?: HairColor | { color: HairColor; style: number } | null;
    lips?: boolean;
    shirt?: ShirtColor | { color: ShirtColor; style: number } | null;
    pants?: PantsColor;
    shield?: ShieldColor | { color: ShieldColor; style: number } | null;
    hat?: number;
  }): Phaser.RenderTexture {
    options = assign(
      {
        skin: SkinColor.White,
        lips: false,
      },
      options
    );
  • Как упоминалось выше, мне нравилась невозможность обнуления… но мне все же удавалось несколько раз задействовать NPE, дальше в программе, чем мне хотелось бы (см. Принцип отказоустойчивости). Я бы хотел, чтобы у TypeScript был режим разработки во время выполнения, аналогичный Dart's DartDevCompiler, компиляторы JavaScript, предназначенные только для разработки, вставляют проверки времени выполнения там, где это необходимо, чтобы быстро выйти из строя. Также, похоже, есть некоторые дыры в целом, например, TypeScript не понимает каждый синтаксис для проверки, если что-то не является нулевым:
let shield = options.shield;
if (typeof shield === 'number') {
  parts.push(this.shield(shield));
} else {
  parts.push(this.shield(shield!.color, shield!.style));
}
  • Структурные типы / анонимные типы были одновременно и удовольствием, и проклятием. Это было действительно привлекательно, в первую очередь от Java / Dart, чтобы избежать необходимости объявлять строго типизированный класс для локальной функции, но это также сделало меня немного параноиком, потому что я перестал понимать, действительно ли что-то было парой x, y, или, если это был более тяжелый класс Phaser.Sprite, который имел свойства x и y с одинаковыми типами. Я мог бы остановить сборщик мусора / удерживать объекты, которые кажутся «дешевыми», но не таковыми (таким образом мы получили несколько утечек памяти!).
  private getEmptyCells(): Array<{ x: number; y: number }> {
    const arr: Array<{ x: number; y: number }> = [];
    for (let xx = 0; xx < this.w; xx++) {
      for (let yy = 0; yy < this.h; yy++) {
        if (this.collisions[xx][yy] === 1) {
          arr.push({ x: xx, y: yy });
        }
      }
    }
    return arr;
  }
  • Типы Union немного сбивали нас с толку, хотя я намеренно пытался ими воспользоваться, просто чтобы где-то их использовать. В некоторых случаях мне следовало использовать типы пересечений вместо типов объединения при отражении, поэтому я сохраняю это как «Мех», потому что я мог бы оценить это выше, если бы попробовал еще раз. Фактически, после написания нескольких типов объединения я действительно просто хотел статически управляемую перегрузку, потому что я делал разные вещи в зависимости от типа, и по иронии судьбы TypeScript не очень хорош для проверки того, что такое тип во время выполнения.
  • Горячая перезагрузка модуля (HMR) Webpack на самом деле не использовалась в моем проекте, в первую очередь потому, что я не тратил время на выяснение того, как ее настроить. Я как бы хотел (надеялся?), Чтобы он просто работал по умолчанию, но вы должны написать хуки, чтобы использовать его. Он действительно возвращается к автоматической перезагрузке браузера, что было достаточно для того, что мы писали, но вы каждый раз теряете состояние.
  • Webpack как система сборки был в порядке. Это была одна из тех вещей, когда она работала, она работала отлично, а когда не работала, мне приходилось иногда прибегать к часу или более поиску в Google. Перед выпуском игры мне пришлось удалить некоторые аудиоресурсы, которые webpack не мог найти на диске по какой-либо причине, даже если они находились рядом с ресурсами, которые он мог найти.
  • Загадочные сообщения об ошибках. Сообщения об ошибках были последовательными и быстрыми, мне очень нравятся две вещи, но похоже, что все они были написаны теоретиками типа, а не пользователями. Например, давайте еще раз рассмотрим ошибку, не допускающую обнуления, которую я описал выше:
function foo(x?: number) {
  // Type 'number | undefined' is not assignable to type 'number'.
  let y: number = x;
}

Плохой

  • Из-за отсутствия унифицированного инструментария (и нехватки времени) нам пришлось полностью отказаться от написания каких-либо тестов. Jest ничего не знал о webpack, поэтому не мог использовать скомпилированный TS. Не было четкого способа использовать для этого плагин, и примерно через 30 минут мы сдались.
  • Использование слова «это». везде вроде сводили меня с ума:
  public update(): void {
    if (this.rKey.isDown) {
      game.worldState.resetKillCounts();
      this.state.start('Main');
    }
  }
  • Система модулей / пакетов TS / ES5 / ES6 чрезвычайно сбивает с толку. Я получаю конечную цель, но поскольку все устаревшие режимы должны поддерживаться, я никогда не мог сказать, каким образом что-то импортировать. Например, у нас есть 4 различных способа загрузки модулей в базу кода:
// Not a module (global), but we declare using .d.ts, and webpack works?
import * as DungeonFactory from 'dungeon-factory';

// Also not a module. We have to tell webpack to "make it one" on the fly:
// module: {
//  { test: /phaser-split\.js$/, loader: 'expose-loader?Phaser' },
// }
import * as Phaser from 'phaser-ce';

// OK, this is actually a module with a bunch of exported functions.
import { forIn } from 'lodash';

// A module with a "default" export (doesn't need { … } notation)
import Cell from './cell';
  • Конфигурация Webpack: здесь я дам слабину Webpack - он действительно настраивается. Очень сильно. Вероятно, вы могли бы гипотетически заставить его делать все, что захотите. Все это имеет обратную сторону - отсутствие герметичной системы сборки и необходимость глубоко понимать, могут ли плагины / загрузчики, которые вы используете, изменять глобальное состояние таким образом, что это приведет к поломке других. Но в конечном итоге это было для меня огромным недостатком, исходящим от Bazel, Maven и Gradle. Мне не нужна была гибкость - мне нужен был один простой проверенный способ работы - но вместо этого мне пришлось искать в Google «TypeScript + Phaser + Webpack», просматривать кучу примеров кода и собирать что-то, пока это не сработает.

TypeScript как язык - это (чрезвычайно) хороший слой краски поверх JavaScript, и как только появится единая унифицированная модульная система с некоторыми более зрелыми инструментами сборки (или Bazel станет большим извне) и экосистемой пакетов, которая использует его преимущества Я видел, что это очень мощная и жизнеспособная среда для всех.

Существует не так много сред и экосистем, в которых можно было бы попросить 4 человека с разным опытом программирования взломать рабочую игру за 2 дня. TypeScript + Webpack определенно один!