UNPKG

universal-emoji-parser

Version:

This tool allow parse unicode and emoji codes to html images using emojilib && Twemoji CDN

436 lines (305 loc) 15.4 kB
--- 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.