---
name: typescript-strict-style
description: TypeScript strictness rules enforced by tsconfig.json + ESLint, plus the patterns the existing code follows
---

# Skill: `typescript-strict-style`

A reference for the TypeScript style enforced in Universal Emoji Parser. Read this when adding new code to `src/` or when ESLint complains about something you didn't expect.

For the prose version (rules + rationale), see [`docs/STANDARDS.md`](../../docs/STANDARDS.md). This skill focuses on patterns and idioms.

## tsconfig.json key settings

```json
{
  "strictNullChecks": true,
  "noImplicitAny": true,
  "noUnusedLocals": true,
  "noUnusedParameters": true,
  "declaration": true,
  "module": "commonjs",
  "moduleResolution": "node",
  "esModuleInterop": true,
  "resolveJsonModule": true,
  "downlevelIteration": true,
  "skipLibCheck": true,
  "removeComments": true
}
```

Implications for new code:

| Setting                                 | What it forces                                                                                             |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `strictNullChecks: true`                | Every nullable union (`T \| undefined`) must be handled with `?.`, `??`, narrowing, or explicit type guard |
| `noImplicitAny: true`                   | Every parameter/return must be annotated or inferable                                                      |
| `noUnusedLocals` / `noUnusedParameters` | Dead code fails the build. Prefix unused params with `_` if you must keep them (rare)                      |
| `declaration: true`                     | `tsc --build` emits `.d.ts` for every public export — keep return types stable                             |
| `resolveJsonModule: true`               | `import emojiLibJson from './lib/emoji-lib.json'` works                                                    |
| `removeComments: true`                  | Comments in `dist/index.js` are stripped at build time. JSDoc still appears in `.d.ts`                     |

`strict` itself isn't on (would also enable `strictFunctionTypes`, `strictPropertyInitialization`, etc.). The granular settings above are the explicit subset.

## Patterns from the existing code

### Typed module-level constants

```ts
export const DEFAULT_EMOJI_CDN: string = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/'
export const emojiLibJsonData: EmojiLibJsonType = emojiLibJson
```

Even where TypeScript can infer the type, **public exports are annotated explicitly**. This stabilizes the `.d.ts` output — a future change that affects inference (e.g., switching the JSON catalog to a different shape) doesn't silently shift the public types.

For internal constants (`const x = 5` inside a function), inference is fine.

### Object-literal "class" pattern

```ts
const uEmojiParser: UEmojiParserType = {
  getEmojiObjectByShortcode(shortcode: string): EmojiType | undefined { ... },
  getDefaultOptions(options?: EmojiParseOptionsType): EmojiParseOptionsType { ... },
  __parseEmojiToHtml(text: string, emojiCDN?: string): string { ... },
  parseToHtml(text: string, emojiCDN?: string): string { ... },
  // ...
}
```

The package uses an object literal annotated with the interface, not a class. Reasons:

- The "instance" is a singleton — there's no construction. A class with `static` methods would work but adds ceremony
- TypeScript's interface conformance check verifies all methods are present
- The dual-export shape (`module.exports = uEmojiParser`) works naturally with object literals

When adding a new method, declare it on the interface in `src/lib/type.ts` first, then add the implementation. TypeScript will surface the missing method as an error, which you can fix by adding it.

### Method signatures with explicit types

```ts
parseToHtml(text: string, emojiCDN?: string): string {
```

Not:

```ts
parseToHtml(text, emojiCDN) {  // ❌ noImplicitAny error
```

Or:

```ts
parseToHtml(text: string, emojiCDN?: string) {  // works but inferred return type may drift
```

Always annotate parameters and return types on public methods.

### `this` inside methods

```ts
parseToHtml(text: string, emojiCDN?: string): string {
  text = this.parseToUnicode(text)
  return this.__parseEmojiToHtml(text, emojiCDN)
}
```

`this` refers to the object literal. TypeScript types `this` correctly because of the `UEmojiParserType` annotation on the const declaration. If you destructure a method (`const { parseToHtml } = uEmojiParser`), `this` is lost — but consumers normally call via the object, so this isn't a real issue.

### Optional parameters with defaults

```ts
getDefaultOptions(options?: EmojiParseOptionsType): EmojiParseOptionsType {
  options = {
    emojiCDN: options && Object.getOwnPropertyDescriptor(options, 'emojiCDN')
      ? String(options.emojiCDN)
      : undefined,
    parseToHtml: options && Object.getOwnPropertyDescriptor(options, 'parseToHtml')
      ? Boolean(options.parseToHtml)
      : true,
    parseToUnicode: options ? Boolean(options.parseToUnicode) : false,
    parseToShortcode: options ? Boolean(options.parseToShortcode) : false,
  }
  return options
}
```

The pattern: accept `options?: T`, build a fully-populated result, return it. **Don't** mutate the input — the function builds a new object every call.

The `Object.getOwnPropertyDescriptor` trick is documented in [`docs/STANDARDS.md`](../../docs/STANDARDS.md#object-option-merge-pattern). It distinguishes "explicitly undefined" from "not passed."

### Internal helpers prefixed with `__`

```ts
__parseEmojiToHtml(text: string, emojiCDN?: string): string { ... }
```

Two underscores at the front mark this as **conventionally private** — implementation detail, may change without notice. It's still typed in `UEmojiParserType` (so it's reachable through the public API), but the prefix tells consumers "use the public method instead."

JavaScript / TypeScript don't have a hard `private` for object literals. The underscore is the convention.

### Type imports separate from value imports

```ts
import { EmojiLibJsonType, EmojiParseOptionsType, EmojiType, TwemojiEntity, UEmojiParserType } from './lib/type'
import emojiLibJson from './lib/emoji-lib.json'
import { parse } from '@twemoji/parser'
```

Notice the `type.ts` import is grouped with other value-style imports, even though only types come from it. TypeScript handles this fine — the types are erased at compile time.

For stricter projects you could write:

```ts
import type { EmojiLibJsonType, EmojiParseOptionsType, ... } from './lib/type'
```

The `type` modifier ensures the import is type-only, so TypeScript can elide it in the emitted JS. The package doesn't use this — it works either way. If you add `import type` for new files, that's fine; don't refactor existing imports just for consistency.

### `interface` over `type` for public types

`type.ts` uses `interface` for every public type:

```ts
export interface EmojiType {
  name: string
  slug: string
  // ...
}
```

Reasons:

- Interfaces support **declaration merging** — consumers can extend in their own `.d.ts`
- TypeScript error messages reference interface names cleanly
- IDE hover shows "interface" — signals "part of the API"

Reserve `type` for unions and mapped types:

```ts
type EmojiKey = keyof EmojiLibJsonType // mapped from another type
type ParseResult = string | undefined // union — no interface possible
```

### Index signatures on the catalog type

```ts
export interface EmojiLibJsonType {
  [key: string]: EmojiType
}
```

The catalog is `{ [unicode_char]: EmojiType }`. The index signature handles arbitrary string keys.

Trade-off: TypeScript can't tell you "the key 😎 exists" at compile time — every lookup is `EmojiType | undefined`. The runtime code handles the undefined case via `?.char` checks.

### `Array<T>` vs `T[]`

The codebase uses `Array<T>`:

```ts
keywords: Array<string>
const entitiesFound: Array<string> = []
```

ESLint `@typescript-eslint/array-type` could enforce one or the other. Currently it's not configured, so both work. **For consistency, use `Array<T>` in new code** — it matches the existing style.

### Optional fields with `?:`

```ts
export interface EmojiType {
  // ...
  keyword_index_found?: number
}
```

The `?:` makes the field optional in the interface. This emits the field as `keyword_index_found?: number` in the `.d.ts` — consumers know it might be missing.

Use sparingly — every optional field is a value the consumer has to handle.

## ESLint rules in detail

`eslint.config.mjs` composes `@eslint/js` recommended + `typescript-eslint` recommended + `eslint-plugin-prettier/recommended`.

Custom rules:

```json
{
  "no-console": 2,
  "@typescript-eslint/no-inferrable-types": "off",
  "@typescript-eslint/no-non-null-assertion": "off",
  "@typescript-eslint/ban-ts-comment": "off",
  "semi": [2, "never"]
}
```

### `no-console: 2`

`console.*` is an error in `src/`. The package is a library — calling `console.log` from inside it leaks log lines into every consumer's output.

`test/` is linted too. If you need to debug a test, use `console.log` temporarily and remove it before committing — ESLint enforces `no-console` there as well.

### `@typescript-eslint/no-inferrable-types: 'off'`

Some rules consider `const x: number = 5` redundant ("the type can be inferred"). We allow it because:

- Public exports always annotate types explicitly (stability)
- Even local annotations can clarify intent for human readers

Not turning this on means you decide case-by-case.

### `@typescript-eslint/no-non-null-assertion: 'off'`

`x!` (non-null assertion) is allowed. Use sparingly — it bypasses the type system. The current codebase uses it in the regenerator's dedup loop:

```ts
emojiLibJson[emojiObjectFound.char].keywords.splice(emojiObjectFound.keyword_index_found!, 1)
```

…where `keyword_index_found` is typed as `number | undefined` but the code's logic guarantees it's set. The alternative would be a `requireNotNull` helper, which adds complexity for a one-off case.

For new code, **prefer `?.` and `??`** over `!`. Reach for `!` only when:

- The invariant is genuinely guaranteed by surrounding logic
- Adding a runtime check would obscure the algorithm

### `@typescript-eslint/ban-ts-comment: 'off'`

`// @ts-ignore`, `// @ts-expect-error`, `// @ts-nocheck` are allowed. Don't abuse them — they're for unavoidable interop, not for silencing real type errors.

### `semi: [2, 'never']`

No semicolons. This reinforces Prettier's `semi: false`. Examples:

```ts
const x = 1                    // ✅
const y = 2                    // ✅

;[x, y].forEach(n => ...)      // ✅ — leading semi when ASI hazard

import x from 'a'              // ✅
const y = 2                    // ✅
```

Prettier inserts the leading `;` automatically when needed (e.g., before a line starting with `(`, `[`, or `+`). Don't add semicolons by hand.

## Prettier rules

`.prettierrc`:

```json
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "es5"
}
```

| Rule                   | Effect                                                                     |
| ---------------------- | -------------------------------------------------------------------------- |
| `semi: false`          | No trailing semicolons                                                     |
| `singleQuote: true`    | `'...'` for strings, `\`...\``for templates. Never`"..."`                  |
| `trailingComma: 'es5'` | Trailing comma in multi-line arrays/objects, but **not** in function calls |

Examples:

```ts
const arr = [
  'a',
  'b',
  'c', // ✅ trailing comma in array
]

const obj = {
  a: 1,
  b: 2, // ✅ trailing comma in object literal
}

fn('a', 'b', 'c') // ✅ no trailing comma in function call (es5 rule)
```

`.editorconfig` adds:

```
indent_style = space
indent_size = 2
end_of_line = lf
max_line_length = 120
```

Prettier respects the 120 limit when reflowing.

## Common ESLint / TypeScript fixes

### "TS6133: 'X' is declared but its value is never read"

Either remove the declaration or use it. If you're keeping it intentionally (e.g., as a parameter for interface conformance), prefix with `_`:

```ts
function noOp(_unused: string): void {
  // genuinely doesn't use _unused
}
```

### "TS2532: Object is possibly 'undefined'"

`strictNullChecks` is fighting you. Options:

```ts
// 1. Optional chaining
emojiLibJsonData[shortcode]?.char

// 2. Default value
emojiLibJsonData[shortcode]?.char ?? ''

// 3. Type guard
const entry = emojiLibJsonData[shortcode]
if (entry) {
  return entry.char
}

// 4. Non-null assertion (last resort)
emojiLibJsonData[shortcode]!.char
```

### "TS7006: Parameter 'X' implicitly has an 'any' type"

Annotate it:

```ts
function f(x: string): void { ... }    // ✅
```

Or, if the type is genuinely unknown, use `unknown` and narrow:

```ts
function f(x: unknown): void {
  if (typeof x === 'string') {
    // x is string here
  }
}
```

### "no-console" violation

Remove the `console.log`. If you really need logging, the package doesn't ship a logger. Open an issue if the use case is real.

### "prettier/prettier" violation

Run `npm run prettier:fix`. If the auto-fix produces something you don't like, the disagreement is between your editor and Prettier; configure your editor to match Prettier.

## Type-driven refactors

### Adding a new option

1. Add the field to `EmojiParseOptionsType` in `src/lib/type.ts`:
   ```ts
   export interface EmojiParseOptionsType {
     // ...
     myNewOption?: boolean
   }
   ```
2. Update `getDefaultOptions` to merge it with a default
3. Update `parse` to act on it
4. Update `docs/API_REFERENCE.md`
5. Tests for both the default and the explicit value

### Adding a new method

1. Add the signature to `UEmojiParserType` in `src/lib/type.ts`
2. TypeScript flags `uEmojiParser` as missing the method — fix by adding the implementation
3. Tests
4. Update `docs/API_REFERENCE.md`

### Changing a return type

This is a breaking change in the `.d.ts`. Treat as a major bump unless:

- The new return type is a subtype (no consumer code breaks)
- The change is internal only (a method prefixed with `__`)

## What we don't enforce

- **No JSDoc lint** — JSDoc is welcome but not required. The current `src/index.ts` is light on JSDoc; adding more is welcome
- **No file-name lint** — `index.ts`, `type.ts`, `emoji-lib.json` follow conventions but no rule enforces them
- **No max-function-length** — functions in `src/index.ts` are all small; if one grows huge, prefer splitting on its own merits

## Future hardening ideas

If you wanted to harden the TypeScript setup further:

- Enable full `strict: true` (would also activate `strictFunctionTypes`, `strictPropertyInitialization`, `alwaysStrict`)
- Enable `@typescript-eslint/strict-boolean-expressions` (forces explicit nullish checks instead of truthy/falsy)
- Enable `@typescript-eslint/no-explicit-any` as `error` instead of off
- Add `@typescript-eslint/explicit-module-boundary-types` to require explicit return types on every exported function

These are nice-to-have but not necessary for the current code's quality. If you adopt any, expect a ~50-line diff in `src/index.ts` to clean up.
