A TypeScript language service plugin. Drop it into any project — no special runtime or file extension required. Violations surface as compiler errors in your editor and CI.
By syntax construct count, ShotScript removes ~74% of what TypeScript allows. We count every distinct way to express the same thing — variable declaration forms, function syntaxes, error-handling patterns — and reduce each category to one. Some disappear entirely; none are banned because they're bad, only because fewer forms is the goal.
Type structure
type Role = 'admin' | 'member' | undefined interface Timestamps { createdAt: Date updatedAt?: Date } interface BaseUser { id: number email: string avatar?: string | null metadata?: Record<string, any> } type User = BaseUser & Timestamps & { role?: Role deletedAt?: Date | null } // role: 4 states · 4 null forms · fields across 3 decls
type Role = 'admin' | 'member' type Timestamps = { readonly createdAt: Date readonly updatedAt: Date | null } type BaseUser = { readonly id: number readonly email: string readonly avatar: string | null readonly metadata: Record<string, unknown> } type User = { readonly base: BaseUser readonly timestamps: Timestamps readonly role: Role | null readonly deletedAt: Date | null } // role: 3 states · 1 null form · all fields explicit
Async + error handling
async function getUser( id?: number ): Promise<User | undefined> { if (!id) return undefined try { const res = await fetch(`/users/${id}`) if (!res.ok) throw new Error( `HTTP ${res.status}` ) const data = await res.json() as any return data as User } catch (e) { console.error(e) return undefined } } // failure is invisible to callers · `as` silences the checker
import { safeFetch, jsonParse } from 'shotscript/std' import type { PromiseResult } from 'shotscript/std' async function getUser( id: number ): PromiseResult<User> { const [res, fetchErr] = await safeFetch( `/users/${id}` ) if (fetchErr !== null) return [null, fetchErr] return jsonParse<User>(await res.text()) } // every failure is in the signature · no `as`, no `throw`
function declarations or expressions —
they show up in stack traces, are grep-able, and are
testable in isolation.
T | null and handle the null branch
explicitly inside the body.
async function with no
await is banned. Remove
async and return
Result<T> instead of
PromiseResult<T>.
[null, new Error(...)] — errors belong
in the type signature, not in hidden control flow.
try/catch is banned. Wrap
third-party throwing code with
toResult or
toPromiseResult from
shotscript/std.
new Promise() and
Promise.resolve/reject/all/race/any/allSettled()
are banned. Use
toPromiseResult() to wrap external
Promise-returning functions.
.then() and
.catch() chains are banned. Use
await with tuple destructuring.
awaited or explicitly discarded with
void fn(). Unhandled promises silently
swallow errors.
[T | null, E | null] tuple must be
destructured immediately —
const [val, err] = fn().
Promise<void>,
PromiseResult<T, E>, or an
explicit tuple form. A bare
Promise<User> hides the failure
path.
var is banned. Use const.
const a = 1, b = 2) are banned. One declaration per statement.
++ and -- are banned. Use
+= 1 and -= 1.
const with a distinct name.
a = b = 1) is
banned.
return statement is
banned.
if/else or extract a named
function.
condition && doThing() for side
effects is banned. Use
if (condition === true) { doThing() }.
condition || doThing() for side
effects is banned. Use an
if block.
if (x !== null), not
if (x) — implicit truthy is banned.
=== null check on a value whose type
can never be null is banned — it's dead code the
type system can prove unreachable.
== and != are banned. Use
=== and !== only.
for...in is banned. Use
for...of Object.keys() or
for...of Object.entries().
do...while is banned. Use a
while loop.
break/continue
with labels are banned. Extract a function and
return instead.
switch case must end with
break, return, or
throw. Implicit fallthrough is banned.
??=,
||=, &&=) are
banned.
const { x = 5 } = obj) are banned. Use explicit null checks.
function*) and
yield are banned.
eval() is banned.
x === x)
is always a bug.
x = x)
is always a bug.
return await x is redundant; use
return x. Since
no-try bans try blocks, there is no
case where return await changes
behavior.
type exclusively — one way to define a
shape.
type for data and plain functions for
behaviour.
abstract classes and members are
banned.
enum is banned. Use an
as const object and a
typeof type alias instead.
any is banned. Use
unknown and narrow explicitly.
value as T) are
banned. Parse and validate at boundaries; return a
result tuple.
value!) are
banned. Check for null explicitly.
@ts-ignore,
@ts-expect-error, and
@ts-nocheck are banned. Fix the type
error.
undefined in type annotations is
banned. The only nullable value is
null.
prop?: T) are
banned. Use prop: T | null.
x?: T) are banned.
Use x: T | null.
readonly. Mutation is explicit, not the
default.
readonly T[]. Not T[], not
Array<T>.
Map<K, V> and
Set<T> in type positions are
banned. Use ReadonlyMap<K, V> and
ReadonlySet<T>.
Array<T> and
ReadonlyArray<T> in annotations
are banned. Use readonly T[].
A & B) are
banned. Spell out the combined fields, or compose by
value.
T extends U ? X : Y)
are banned.
{ [K in keyof T]: ... })
are banned.
prefix-${T} `) are banned.
infer inside conditional types is
banned.
[...T]) are
banned.
[value: T, err: E] not
[T, E].
[k: string]: T) are
banned. Use Map<K, V>.
Partial, Required,
Record, InstanceType,
ConstructorParameters, and
ThisType are banned. Spell out the
shape.
Readonly<T> wrapper is banned.
Mark each property readonly directly.
String,
Number, Boolean,
Symbol, BigInt) are
banned. Use the lowercase primitives.
new (...args): T) are banned.
Function type is banned. Use an
explicit call signature type.
object type is banned. Use an
explicit type shape.
{} in annotations
is banned.
symbol and unique symbol
are banned. Their two practical uses — branded types
and well-known symbols (Symbol.iterator
etc.) — are both unavailable in ShotScript: branding
uses a string phantom field
({ readonly __brand: B }), and
generators/iterables are banned.
true | false is banned — it is just
boolean.
namespace
/ module declarations) are banned.
this is banned in all forms — no method
context, no this parameters.
xs: readonly number[] and
{ readonly x: number }.
require() is banned. ESM
import only.
index file or a bare
directory is banned. Import the specific file.
export {} statements that serve
no purpose are banned.
export * from './x' and
export * as ns from './x' are banned;
use named re-exports.
import './setup'
with no bindings) are banned; name what you import.
import(...) expressions are
banned; use a static import.
+ is banned
when a template literal would work. Use ` hello ${name} `.
[1,,3]) are
banned. Use null for an explicit empty
slot: [1, null, 3].
!!value is banned. Use
Boolean(value).
+ for coercion is banned. Use
Number(value).
parseInt and
parseFloat are banned. Use
Number(str).
JSON.parse,
JSON.stringify, fetch,
structuredClone, BigInt(),
new RegExp(), and
new Date() are banned. Use the safe
wrappers from shotscript/std.
Proxy, Reflect,
Object.assign,
Object.create,
Object.defineProperty, and similar
metaprogramming APIs are banned.
new String(),
new Number(),
new Boolean() are banned.
new on user-defined constructors is
banned. No classes means no new.
delete operator is banned. Build a
new object without the key.
in operator is banned. Use explicit
property checks or discriminated unions.
&,
|, ^, ~,
<<, >>,
>>>) are banned.
arguments object is banned. Use
explicit rest parameters.
void expr is banned except as an
explicit promise discard:
void someCall() is the only permitted
form.
{}) on functions,
if, while, etc. are
banned.
const {} = x, const [] = x) are banned.
{ ... }
not attached to control flow) are banned.
import { x as x }) is banned.
return at the end of a
void function is banned.
"a" + "b") is banned — just write "ab".
{ ["x"]: 1 }) are banned.
.sort(), .reverse(),
.splice(), .push(),
.pop(), .shift(),
.unshift(), .fill(),
.copyWithin() are banned. Use ES2023
immutable alternatives: toSorted,
toReversed, toSpliced,
with, or spread.
Object.assign(...) is banned; use
object spread { ...a, ...b } instead.
get/set accessors in
object literals are banned; use plain properties or
functions.
isNaN,
isFinite, and
hasOwnProperty are banned. Use
Number.isNaN,
Number.isFinite, and
Object.hasOwn.
.hasOwnProperty(),
.isPrototypeOf(),
.propertyIsEnumerable() as method calls
are banned. Use Object.hasOwn etc.
npm install --save-dev shotscript, then add {"name": "shotscript/plugin"} to the plugins array in compilerOptions in tsconfig.json. Full ShotScript setup →// tsconfig.json { "compilerOptions": { "plugins": [{ "name": "shotscript/plugin" }] } }
Exits 0 when clean, 1 on
violations. Add --json for machine-readable
output.