Optional Parameters

? and = default both survive conversion, undefined is not null, and every function can tell you at runtime which of its parameters are required.

TypeScript has two ways to make a parameter optional: greeting?: string, which may be undefined, and punctuation: string = '!', which has a default. Both convert to TJS, and both keep their meaning.

By default, converted TypeScript validates nothing: it behaves exactly like the JavaScript tsc would emit. That is deliberate. Conversion should not change what your program does, and full TJS changes more than validation (for example, == stops coercing, and object defaults merge, as described at the end of this page). The first line of the example, /* @tjs TjsStrict */, opts this one file in to full TJS, and the types become checks that run on every call.

Whether or not you opt in, each function also carries a description of its parameters at runtime: which are required, and what the defaults are. The example shows both.

/* @tjs TjsStrict */

function createGreeting(
  name: string,
  greeting?: string,
  punctuation: string = '!'
): string {
  const g = greeting ?? 'Hello'
  return `${g}, ${name}${punctuation}`
}

// Every way TypeScript allows you to call it still works
console.log(createGreeting('World'))
console.log(createGreeting('World', 'Hi'))
console.log(createGreeting('World', undefined, '?'))
console.log(createGreeting('World', 'Hey', '...'))

// In real code, bad values arrive from parsed JSON, from JavaScript callers, or through
// `any`; `as any` stands in for that here. Calls that bypass TypeScript's checks now fail
// when they run. The error is returned, not thrown.
function show(result: unknown) {
  // `unknown` accepts anything, including the returned error
  console.log(result instanceof Error ? `error: ${result.message}` : result)
}
show(createGreeting('World', null as any)) // optional means "may be undefined", not "may be null"
show(createGreeting('World', 42 as any))
show(createGreeting('World', 'Hi', 3 as any))

// The function carries its own parameter metadata at runtime
console.log('\nFunction metadata:')
for (const [param, info] of Object.entries((createGreeting as any).__tjs.params)) {
  const { required, default: fallback } = info as any
  console.log(
    `  ${param}: ${required ? 'required' : 'optional'}` +
      (!required && 'default' in (info as any) ? `, default ${JSON.stringify(fallback)}` : '')
  )
}

It prints:

Hello, World!
Hi, World!
Hello, World?
Hey, World...
error: Expected string | undefined for '<source>:3:createGreeting.greeting', got null
error: Expected string | undefined for '<source>:3:createGreeting.greeting', got number
error: Expected string for '<source>:3:createGreeting.punctuation', got number

Function metadata:
  name: required
  greeting: optional
  punctuation: optional, default "!"

The start of each error message names the source, the line of the function and the parameter that failed.

What the converter writes

Here is the signature before and after conversion:

// TypeScript
function createGreeting(name: string, greeting?: string, punctuation: string = '!'): string

// TJS, as the converter writes it
function createGreeting(name: string, greeting: string | undefined, punctuation = '!'):! string

TJS describes types with example values: name: 'Alice' means "a string, like this one". A bare type name such as string is also accepted and means the same thing, which is why the converter can leave name: string as it is. The :! before the return type means "this is the type of the result, not a worked example to test".

What to notice

The metadata

Every TJS function has a __tjs property. Its params entry maps each parameter name to a description. For punctuation it is:

{ type: { kind: 'string' }, required: false, default: '!' }

greeting has required: false and no default key; name has required: true. A required parameter also carries default: null, which is why the example prints a default only for optional parameters. Next to params, __tjs holds returns (the return type) and source (the file and line).

__tjs is added by the transpiler, so TypeScript does not know about it; that is what the as any casts are for. To turn it into something standard, functionMetaToJSONSchema(fn.__tjs) from tjs-lang/lang gives a JSON Schema for the function's input and output, built from the code that is actually running. From Zod to TJS uses it.

The gotcha: null that used to work

With strictNullChecks, TypeScript already refuses null for greeting?: string. What changes under TjsStrict is that the rule is now enforced at runtime. The body uses ??, which treats null like undefined, so in the plain JavaScript output createGreeting('World', null) quietly printed Hello, World!. Under TjsStrict the same call returns an error, as the example shows.

An optional parameter accepts undefined, because that is what an omitted argument is, but not null. If a caller may pass null on purpose, say so in the type: greeting?: string | null.

TjsStrict also changes object defaults

Under TjsStrict an object default becomes a dictionary default: each member is defaulted separately, and a partial argument is merged with the defaults. In function f(opts = { x: 0, y: 0 }), calling f({ x: 5 }) gives { x: 5, y: 0 }. Without TjsStrict, converted TypeScript keeps JavaScript's all-or-nothing default and gives { x: 5 }. This is a behaviour change, so check your object defaults when you opt a file in. See Converting a TypeScript Project.