ts2workflow converts Typescript source code to GCP Workflows YAML syntax. Only a subset of Typescript language features are supported. This page documents supported Typescript features and shows examples of the generted Workflows YAML output.
Most functions provided by a Javascript runtime (console.log()
, setInterval()
, etc) are not available.
Type annotations are allowed. Type checking is done by the compiler but the types dont't affect the generated Workflows code.
Semicolon can be used as optional statement delimitter.
number
: Integer (64 bit, signed) or double (64 bit, signed floating point number)string
:"my beautiful string"
, both single or double quotes are acceptedboolean
:true
/false
bytes
null
- Array:
[1, 2, 3]
- Map:
{temperature: -12, unit: "Celsius"}
[].map()
and [].concat()
are not available.
undefined
.
Map keys can be identifiers or strings: {temperature: -12}
or {"temperature": -12}
. Trailing commas are allowed.
undefined
.
It is not possible to construct a bytes object expect by calling a function that returns bytes (e.g. base64.decode). It is not possible to do anything else with a bytes object than to assign it to a variable and to pass it one of the functions that take bytes type as input variable (e.g. base64.encode).
In addition to the literal null
, the Typescript undefined
value is also treated as null
in Workflows YAML.
Expressions that combine variables with operators such as +
, >
, ==
perform implict type conversions according to the rules listed on GCP Workflows documentation. For example, applying +
to a string and a number concatenates the values into a string.
if (var != null) {...}
. Relying in an implicit conversion, such as if (var) {...}
, results in a TypeError at runtime.
Most Typescript expressions work as expected.
Examples:
a + b
args.users[3].id
name === 'Bean'
sys.get_env('GOOGLE_CLOUD_PROJECT_ID')
Operator | Description |
---|---|
+ | arithmetic addition and string concatenation |
- | arithmetic subtraction or unary negation |
* | multiplication |
/ | float division |
% | remainder division |
==, === | equal to (both mean strict equality) |
!=, !== | not equal to (both mean strict equality) |
<, >, <=, >= | inequality comparisons |
&&, ||, ! | logical operators |
in | check if a property is present in an object |
?? | nullish coalescing |
?. | optional chaining |
? : | conditional operator |
The precendence order of operators is the same as in GCP Workflows.
See expression in GCP Workflows for more information.
The expression
x > 0 ? 'positive' : 'not positive'
is converted to an if() expression:
${if(x > 0, "positive", "not positive")}
The expression
x ?? 'default value'
is converted to a default() expression:
${default(x, "default value")}
null
or undefined
.
The optional chaining expression
data.user?.name
is converted to a map.get() expression:
${map.get(data, ["user", "name"])}
Template literals are strings that support string interpolation. For example, Hello ${name}
.
Typescript function
s are converted to subworkflow definitions.
The program code must be written inside workflow blocks. Only function
and import
declarations are allowed on the top level of a source code file. Functions can only be defined at the top level of a source file, not inside functions or in other nested scopes.
The workflow execution starts from the subworkflow called "main".
function main(): void {
const a = 1
}
function anotherWorkflow(): number {
const b = 10
return 2 * b
}
Workflows can have parameters:
function multiply(firstFactor: number, secondFactor: number): number {
return firstFactor * secondFactor
}
Parameters can be optional and have a default value that is used if a value is not provided in a subworkflow call:
function log(x, base = 10) {
return 'Should compute the logarithm of x'
}
return 'Success'
The returned value can be an expression:
return firstFactor * secondFactor
The statement
const projectId = sys.get_env('GOOGLE_CLOUD_PROJECT_ID')
is converted to an assign step:
- assign1:
assign:
- projectId: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
This syntax can be used to call standard library functions, subworkflows or connectors. Note that Javascript runtime functions (such as fetch()
, console.error()
or new XMLHttpRequest()
) are not available on Workflows.
GCP Workflows language has two ways of calling functions and subworkflows: as expression in an assign step or as call step. They can mostly be used interchangeably. However, blocking calls must be made as call steps. The transpiler tries to automatically output a call step when necessary.
It is also possible to force a function to be called as call step. This might be useful, if the transpiler fails to output call step when it should, or if you want to use named parameters. For example, the following Typescript program
import { call_step, sys } from 'ts2workflows/types/workflowslib'
function main() {
call_step(sys.log, {
json: { message: 'Hello log' },
severity: 'INFO',
})
}
is converted to call step:
main:
steps:
- call1:
call: sys.log
args:
json:
message: Hello log
severity: INFO
Some Workflows standard library functions have names that are reserved keywords in Typescript. Those functions must be called with alternative syntax in ts2workflows source code:
- To generate a call to
default()
in Workflows code, use the nullish coalescing operator??
. - To generete a call to
if()
in Workflows code, use the ternary operatora ? b : c
.
The statement
const name = 'Bean'
is converted to an assign step:
- assign1:
assign:
- name: Bean
Compound assignments are also supported:
total += 1
is converted to
- assign1:
assign:
- total: ${total + 1}
The statement
if (hour < 12) {
part_of_the_day = 'morning'
} else if (hour < 17) {
part_of_the_day = 'afternoon'
} else if (hour < 21) {
part_of_the_day = 'evening'
} else {
part_of_the_day = 'night'
}
is converted to a switch step
- switch1:
switch:
- condition: ${hour < 12}
steps:
- assign1:
assign:
- part_of_the_day: morning
- condition: ${hour < 17}
steps:
- assign2:
assign:
- part_of_the_day: afternoon
- condition: ${hour < 21}
steps:
- assign3:
assign:
- part_of_the_day: evening
- condition: true
steps:
- assign4:
assign:
- part_of_the_day: night
Typescript switch statements are transpiled to chains of conditions. For example, this statement
let b
switch (a) {
case 1:
b = 'first'
break
case 2:
b = 'second'
break
default:
b = 'other'
}
return b
is converted to
steps:
- assign1:
assign:
- b: null
- switch1:
switch:
- condition: ${a == 1}
next: assign2
- condition: ${a == 2}
next: assign3
- condition: true
next: assign4
- assign2:
assign:
- b: first
- next1:
next: return1
- assign3:
assign:
- b: second
- next2:
next: return1
- assign4:
assign:
- b: other
- return1:
return: ${b}
The fragment
let total = 0
for (const i of [1, 2, 3]) {
total += i
}
is converted to the following for loop statement
steps:
- assign1:
assign:
- total: 0
- for1:
for:
value: i
in:
- 1
- 2
- 3
steps:
- assign2:
assign:
- total: ${total + i}
while
and do...while
loops are also supported:
const total = 0
let i = 5
while (i > 0) {
total += i
i -= 1
}
let k = 5
do {
total += k
k -= 1
} while (k > 0)
for...in
loops are not supported.
Breaking out of a loop:
let total = 0
for (const i of [1, 2, 3, 4]) {
if (total > 5) {
break
}
total += i
}
Continuing from the next iteration of a loop:
let total = 0
for (i of [1, 2, 3, 4]) {
if (i % 2 == 0) {
continue
}
total += i
}
The special function parallel
can be used to execute several code branches in parallel. The code blocks to be executed in parallel are given as an array of () => void
functions. For example, the following code
parallel([
() => {
http.post('https://forum.dreamland.test/register/bean')
},
() => {
http.post('https://forum.dreamland.test/register/elfo')
},
() => {
http.post('https://forum.dreamland.test/register/luci')
},
])
is converted to parallel steps
- parallel1:
parallel:
branches:
- branch1:
steps:
- call1:
call: http.post
args:
url: https://forum.dreamland.test/register/bean
- branch2:
steps:
- call2:
call: http.post
args:
url: https://forum.dreamland.test/register/elfo
- branch3:
steps:
- call3:
call: http.post
args:
url: https://forum.dreamland.test/register/luci
The branches can also be subworkflow names:
parallel([my_subworkflow1, my_subworkflow2, my_subworklfow3])
An optional second parameter is an object that can define shared variables and concurrency limits:
let numPosts = 0
parallel(
[
() => {
n = http.get('https://forum.dreamland.test/numPosts/bean')
numPosts = numPosts + n
},
() => {
n = http.get('https://forum.dreamland.test/numPosts/elfo')
numPosts = numPosts + n
},
],
{
shared: ['numPosts'],
concurrency_limit: 2,
},
)
The following form of parallel
can be called to execute iterations in parallel. The only statement in the function body must be a for
loop.
parallel(() => {
for (const username of ['bean', 'elfo', 'luci']) {
http.post('https://forum.dreamland.test/register/' + username)
}
})
is converted to parallel iteration:
- parallel1:
parallel:
for:
value: username
in:
- bean
- elfo
- luci
steps:
- call1:
call: http.post
args:
url: ${"https://forum.dreamland.test/register/" + username}
The shared variables and concurrency limits can be set with the following syntax:
let total = 0
parallel(
() => {
for (const i of [1, 2, 3, 4]) {
total += i
}
},
{
shared: ['total'],
concurrency_limit: 2,
},
)
The statement
try {
http.get('https://visit.dreamland.test/')
} catch (err) {
return 'Error!'
}
is compiled to the following try/except structure
- try1:
try:
steps:
- call1:
call: http.get
args:
url: https://visit.dreamland.test/
except:
as: err
steps:
- return1:
return: Error!
The error variable and other variables created inside the catch block are accessible only in that block's scope (similar to the variable scoping in Workflows).
It is possible to set a retry policy for a try-catch statement. Because Typescript does not have retry
keyword, the retry is implemented by a special retry_policy
function. It must be called immediately after a try-catch block. A call to the retry_policy
is ignored elsewhere.
The retry_policy
function must be called with parameters defining a retry policy. It can be either a policy provided by GCP Workflows or a custom retry policy. See the GCP documentation for the required parameters for the two policy types.
A sample with a GCP-provided retry policy:
import { http, retry_policy } from 'ts2workflows/types/workflowslib'
function main() {
try {
http.get('https://visit.dreamland.test/')
} catch (err) {
return 'Error!'
}
retry_policy({ policy: http.default_retry })
}
A sample with a custom retry policy:
import { http, retry_policy } from 'ts2workflows/types/workflowslib'
function main() {
try {
http.get('https://visit.dreamland.test/')
} catch (err) {
return 'Error!'
}
retry_policy({
predicate: http.default_retry_predicate,
max_retries: 3,
backoff: {
initial_delay: 0.5,
max_delay: 60,
multiplier: 2,
},
})
}
The above is compiled to the following try/except structure
main:
steps:
- try1:
try:
steps:
- call1:
call: http.get
args:
url: https://visit.dreamland.test/
retry:
predicate: ${http.default_retry_predicate}
max_retries: 3
backoff:
initial_delay: 0.5
max_delay: 60
multiplier: 2
except:
as: err
steps:
- return1:
return: Error!
The statement
throw 'Error!'
is compiled to the following raise block
- raise1:
raise: 'Error!'
The error can be a string, a map or an expression that evaluates to string or map.
Thrown errors can be handled by a try statement.
The transpiler labels output steps with the step type and sequential numbering by default: e.g. assign1
, assign2
, etc. The automatic labels can be overridden by using Typescript labeled statements.
setName: const name = 'Bean'
is converted to a step with the label setName
:
- setName:
assign:
- name: Bean
Type annotations for GCP Workflows standard library functions and expression helpers are provided by importing "ts2workflows/types/workflowslib".
import { sys } from 'ts2workflows/types/workflowslib'
function read_from_env() {
return sys.get_env('GOOGLE_CLOUD_PROJECT_ID')
}
At the moment, type annotations are provided for some connectors but not for all of them.
This section describes the few standard Javascript runtime functions that are available. Most are not.
Array.isArray(arg: any): arg is any[]
Gets converted to the comparison get_type(arg) == "list"
. Unlike a direct call to get_type()
, Array.isArray()
allows the type inference to learn if arg
is array or not.
ts2workflows provides some special functions for implementing features that are not directly supported by Typescript language features. The type annotations for these functions can be imported from ts2workflows/types/workflowslib:
import {
call_step,
parallel,
retry_policy,
} from 'ts2workflows/types/workflowslib'
function call_step<T, A extends any[]>(
func: (...args: A) => T,
arguments: Record<string, unknown>,
): T
The call_step
function outputs a call step.
function parallel(
branches: (() => void)[] | (() => void),
options?: {
shared?: string[]
concurrency_limit?: number
exception_policy?: string
},
): void
The parallel
function executes code blocks in parallel (using parallel step). See the previous sections covering parallel branches and iteration.
function retry_policy(
params:
| {
policy: (exception: unknown) => void
}
| {
predicate: (exception: unknown) => boolean
max_retries: number
backoff: {
initial_delay: number
max_delay: number
multiplier: number
}
},
): void
The retry_policy
function can called right after a try
-catch
block to specify a retry policy. See the section on retrying.
In-line comments start with //
. The rest of the line starting from //
is considered a comment. Multiline comments are defined with the /* */
syntax.
Example:
const var1 = 1 // This is a comment
ts2workflows supports only a subset of all Typescript language features. Some examples that are not (yet) supported by ts2workflows:
- Most functions provided by a Javascript runtime (
console.log()
,setInterval()
, etc) are not available. Only the GCP Workflows standard library functions and connectors are available. - Classes (
class
) are not supported - Arrays and maps are not objects. In particular, arrays don't have methods such as
[].push()
,[].map()
, etc. - Functions (subworkflows) are not first-class objects. Functions can not be assigned to a variable or passed to other functions
- Update expressions (
x++
and similar) are not supported - Destructuring (
[a, b] = func()
) is not supported - and many other Typescript language features are not supported
Some of these might be implemented later, but the goal of ts2workflows project is not to implement the full Typescript compatibility.