marko icon indicating copy to clipboard operation
marko copied to clipboard

Type-checked templates

Open nicolashery opened this issue 7 years ago • 9 comments

Summary

Adding type annotations to JavaScript in order to get compile-time guarantees has become popular with tools like TypeScript and Flow. Would it be feasible to get the same benefits in Marko template files?

Rationale

View engines that are "just JavaScript" like React can easily be type-checked (both TypeScript and Flow actually have out-of-the box support for React).

When working with templates though, you loose the benefit of type-checking as soon as you cross the boundary into the template file. In a type-checked project, this can make refactoring trickier, for example you rename a property but forget to update the template.

On the other hand, templates have the advantage of compile-time optimizations (ex: server-side rendering performance). Being able to have type-checked templates could be a big win. I don't think this has been done yet in the JavaScript community.

Note: My knowledge of compiling templates or type-checking is close to none, so I don't even know if this is feasible :) This issue is intended to start a discussion.

What it could look like

(I'm using TypeScript in my examples but this could very well be translated to Flow.)

Say I have declared these types:

// types.ts

export interface Cat {
  name: string;
  meow: string;
}

export interface Dog {
  name: string;
  woof: string;
}

A TypeScript file would immediately tell me if I'm accessing wrong properties:

// test.ts

import { Cat, Dog } from "./types";

function run(cat: Cat, dog: Dog) {
  // Error: Property 'woof' does not exist on type 'Cat'.
  console.log(cat.name, cat.woof);
  // Error: Property 'meow' does not exist on type 'Dog'.
  console.log(dog.name, dog.meow);
}

But a Marko template wouldn't:

<!-- templates/animals.marko -->

<div>
  <h3>${data.cat.name}</h3>
  <p>${data.cat.woof}</p> <!-- cat.woof is undefined -->
  <h3>${data.dog.name}</h3>
  <p>${data.dog.meow}</p> <!-- dog.meow is undefined -->
</div>

What if I could declare the type of the data my template expects?

<!-- templates/animals.marko -->

<script marko-import>
  import { Cat, Dog } from "../types";

  interface TemplateData {
    cat: Cat;
    dog: Dog;
  }
</script>

<div>
  <h3>${data.cat.name}</h3>
  <p>${data.cat.woof}</p>
  <h3>${data.dog.name}</h3>
  <p>${data.dog.meow}</p>
</div>

And tell Marko to generate type-annotated files?

$ markoc templates --types=typescript

The TypeScript compiler would then catch the errors on the generated JavaScript:

// templates/animals.marko.ts

import marko from "marko";

import { Cat, Dog } from "../types";

interface TemplateData {
  cat: Cat;
  dog: Dog;
};

function create(__helpers) {
  var str = __helpers.s,
      empty = __helpers.e,
      notEmpty = __helpers.ne,
      escapeXml = __helpers.x;

  return function render(data: TemplateData, out) {
    out.w("<div><h3>" +
      escapeXml(data.cat.name) +
      "</h3><p>" +
      // Error: Property 'woof' does not exist on type 'Cat'.
      escapeXml(data.cat.woof) +
      "</p><h3>" +
      escapeXml(data.dog.name) +
      "</h3><p>" +
      // Error: Property 'meow' does not exist on type 'Dog'.
      escapeXml(data.dog.meow) +
      "</p></div>");
  };
}

let template: MarkoTemplate;
template = marko.c(__filename)).c(create);

export default template;

And as I use the template in my type-checked project I would get auto-completion and type errors:

// render.ts

import template from "./templates/animals.marko";
import { Cat, Dog } from "./types";

let cat: Cat;
cat = {
  name: "Felix",
  meow: "Meow!"
};

let dog: Dog;
dog = {
  name: "Rex",
  woof: "Woof!"
};

// Error: Argument of type '{ cat: Dog; dog: Cat; }' is not assignable to parameter of type 'TemplateData'.
//  Types of property 'cat' are incompatible.
//    Type 'Dog' is not assignable to type 'Cat'.
//      Property 'meow' is missing in type 'Dog'.
var html = template.renderSync({
  cat: dog,
  dog: cat
});
console.log(html);

Open questions

  • Source-maps & errors: If a runtime error occurs in the generated JS, can we show the line in the .marko file that caused it?
  • Editor integration: A nice feature of TypeScript is the auto-completion and real-time type-checking. Would it be feasible to have that directly in the the .marko file?

Relevant links

  • https://github.com/Microsoft/TypeScript/issues/5151
  • Original discussion: https://twitter.com/MarkoDevTeam/status/748638615937245185

nicolashery avatar Jul 06 '16 18:07 nicolashery

/CC @philidem @mlrawlings @tindli

Thank you for the detailed write up, @nicolashery! This seems completely doable with some tweaks to the compiler and runtime. For example, we would need to modify the FunctionDeclaration AST node to support extra type information for params.

Personally, I am not yet using TypeScript. I'm not opposed to TypeScript, but I also haven't had a strong desire to start using it. Because of that, I may not be the best person to work on this. I would rather someone who is actively using TypeScript to do the coding, but I can definitely help point you in the right direction. As long as the code changes are clean and well-tested then I have no objections to this proposal.

Regarding open questions:

Source-maps & errors: If a runtime error occurs in the generated JS, can we show the line in the .marko file that caused it?

That may be nice and I think that is also doable without drastic changes. For example, the compiler would need to attach source mappings to parsed expressions. It's definitely something I have thought about, but we haven't had any time to come up with a formal proposal or implement it. With that said, I have found source maps to be somewhat problematic and often end up disabling them (e.g. when doing Atom plugin development with CoffeeScript). Instead, we have focused on making sure the compiled output is extremely readable. For that reason, supporting source maps has been a low priority.

Editor integration: A nice feature of TypeScript is the auto-completion and real-time type-checking. Would it be feasible to have that directly in the the .marko file?

I would have to take a look at how TypeScript autocompletion works for other files, but I suspect this will be challenging. Maybe the template can be compiled to TypeScript and source maps could be used to generate autocompletions as the user is editing the .marko file? It seems like that would probably be too slow, but maybe not.


Please let me know if you are interested in contributing and we can discuss a plan of action.

Any thoughts from others?

patrick-steele-idem avatar Jul 06 '16 18:07 patrick-steele-idem

@patrick-steele-idem @mlrawlings I haven't gotten a change to explore this much, but I saw that Marko v4 is just around the corner (congratz btw! 😄). I definitely don't expect this to make it to v4, but I did want to see if there were any changes in v4 that would make it hard to add TypeScript support in the future (should we want to of course), without any breaking changes.

I've quickly put this together: https://github.com/nicolashery/explore-marko-typescript

You can take a look at how I had to change the original Marko output (src/components/search-results-item.marko.js) to make it useful with TypeScript (src/components/search-results-item.marko.ts).

There are a couple things in there, but the main one that caught my attention is probably Marko's class tag. It is rather prescriptive and also the JS seems to get parsed, so I can't use TypeScript syntax (like I can in Marko's static tag). I also can't use an ES6 class (which reminds me of React switching from React.createClass() to standard ES6 class).

Off the top of my mind, one option could be for the class tag to be an actually ES6 class. Or it could be a component tag instead, that works more like the static tag (i.e. allows any text assumed to be valid JavaScript, and that JS should evaluate to a valid Marko component). Thoughts?

Of course I don't want to distract, I know you guys are focused on shipping v4 and that's the most important. But I thought it could be worth thinking about this now, even if just a little bit.

nicolashery avatar Feb 22 '17 16:02 nicolashery

I think this will be possible once the changes here are hashed out: https://github.com/Microsoft/TypeScript/issues/6508 . They are solving some angular-2-specific problems that should also make it easier to typecheck .marko files, since they are pretty similar in structure.

gilbert avatar Feb 22 '17 17:02 gilbert

Thanks @mindeavor, I hadn't seen that. From skimming through it, I think this will help with the question of editor integration (ex: VSCode Angular Language Service). I looks like it's been open for a while though.

What I had in mind as a first step is more along the lines of Angular's Ahead of Time (AOT) compilation, which outputs TypeScript code that can be used when type-checking the whole app (business logic code + templates):

$ markoc src/ --typescript
Compiling:
  Input:  src/components/search-results-item.marko
  Output: src/components/search-results-item.marko.ts

Compiled 1 templates(s)

$ tsc
src/components/search-results-item.marko.ts(98,26): error TS2339: Property 'pric' does not exist on type 'SearchResultsItem'.

nicolashery avatar Feb 22 '17 18:02 nicolashery

I haven't looked closely, but looks like the plugin support released in TypeScript 2.3 may be of help for type-checking .marko files, and more generally providing editor tooling for Marko (autocompletion, etc.). Here is an example plugin for the Vue.js framework.

nicolashery avatar May 01 '17 14:05 nicolashery

+1

mauricionr avatar Jan 21 '18 20:01 mauricionr

Is there any new progress with this?

j-perezr avatar Mar 20 '18 20:03 j-perezr

any update?

winuxue avatar Jul 13 '18 16:07 winuxue

Svelte works with TypeScript ad type-checks its template files using svelte-check. Perhaps Marko could do something similar to them?

BasixKOR avatar Sep 28 '21 05:09 BasixKOR

This is now fully supported.

Documentation: https://markojs.com/docs/typescript/

See:

  • [https://marketplace.visualstudio.com/items?itemName=Marko-JS.marko-vscode](editor plugin)
  • [https://github.com/marko-js/language-server/tree/main/packages/type-check](type checking ci)

DylanPiercey avatar Apr 21 '23 18:04 DylanPiercey