Skip to content

Please ship TypeScript definitions #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
SnarkBoojum opened this issue Nov 12, 2020 · 6 comments · Fixed by #591
Closed

Please ship TypeScript definitions #303

SnarkBoojum opened this issue Nov 12, 2020 · 6 comments · Fixed by #591

Comments

@SnarkBoojum
Copy link

Some definitions are available here.

Thanks

@ExplodingCabbage
Copy link
Collaborator

Looks like there's a process to deprecate the DefinitelyTyped types that we should follow when we do this - see DefinitelyTyped/DefinitelyTyped#20258 for an example.

@Rednas83
Copy link

Rednas83 commented Oct 9, 2024

Any updates?

@isaachinman
Copy link

@ExplodingCabbage @tmm1 Would you accept a PR to include the types from here directly into this repo?

@ExplodingCabbage
Copy link
Collaborator

@isaachinman sure, that'd save me some work figuring out how to do that and be a great start point.

@isaachinman
Copy link

Spent about 15 minutes updating the rollup config to emit types based on your JavaScript codebase. Unfortunately, those declarations are completely incorrect. You'd need to either rewrite the entire repo in TS, or at least annotate via jsdoc for proper types.

Until then, I think the straightforward thing to do is basically inline the @types/diff content and just publish it from within this package.

I've opened a PR and tested it against my own consuming application.

@ExplodingCabbage
Copy link
Collaborator

ExplodingCabbage commented Mar 28, 2025

So, what I should have done before starting this (over at #591) is just try to correctly type a dummy version of one function like diffChars, specifically making sure it properly handles:

  • the callback option (which makes the function return undefined)
  • the timeout and maxEditDistance options (which make the function possibly return undefined), and
  • the interaction between the two points above (the argument passed to the callback is nullable if and only if timeout or maxEditDistance is provided)

... all complete with some tests to make sure I'm getting it right.

This turns out to be a real pain in the arse, because TypeScript seems to have a lot of bugs with matching calls of overloaded functions to the right overload, and I do not have anything close to a detailed understanding of those bugs.

Here's the test-d.ts file I wrote in a new project (which I then checked with tsd):

import {expectType} from 'tsd';
import {ChangeObject, diffChars} from '.';

const result1 = diffChars('foo', 'bar', {ignoreCase: true});
expectType<ChangeObject<string>[]>(result1);

const result2 = diffChars('foo', 'bar');
expectType<ChangeObject<string>[]>(result2);

const result3 = diffChars('foo', 'bar', {timeout: 100});
expectType<ChangeObject<string>[] | undefined>(result3);

const result4 = diffChars('foo', 'bar', {maxEditDistance: 100});
expectType<ChangeObject<string>[] | undefined>(result4);

const result5 = diffChars('foo', 'bar', cbResult => {
    expectType<ChangeObject<string>[]>(cbResult)
});
expectType<undefined>(result5);

const result6 = diffChars('foo', 'bar', {
    callback: cbResult => {
        expectType<ChangeObject<string>[]>(cbResult);
    }
});
expectType<undefined>(result6);

const result7 = diffChars('foo', 'bar', {
    timeout: 100,
    callback: cbResult => {
        expectType<ChangeObject<string>[] | undefined>(cbResult)
    }
});
expectType<undefined>(result7);

And here's a way to define diffChars, with five overload signatures, that causes the above test file to pass:

export interface ChangeObject<T> {
  value: T
}

interface DiffCharsOptions {
  ignoreCase?: boolean;
}

export interface DiffCharsOptionsNonabortable extends DiffCharsOptions {
  callback?: NonabortableDiffCallback<string>
}

type DiffCharsOptionsAbortable = DiffCharsOptions & AsyncOptions & Partial<AbortableCallbackOption<string>>

interface TimeoutOption {
  timeout: number;
}

interface MaxEditDistanceOption {
  maxEditDistance: number;
}

interface NonabortableCallbackOption<T> {
  callback: NonabortableDiffCallback<T>
}

interface AbortableCallbackOption<T> {
  callback: AbortableDiffCallback<T>
}

export type NonabortableDiffCallback<T> = (result: ChangeObject<T>[]) => void;
export type AbortableDiffCallback<T> = (result: ChangeObject<T>[] | undefined) => void;

type AsyncOptions = TimeoutOption | MaxEditDistanceOption;

export function diffChars(
  oldStr: string,
  newStr: string,
  options: NonabortableDiffCallback<string>
): undefined;
export function diffChars(
  oldStr: string,
  newStr: string,
  options: DiffCharsOptionsAbortable & AbortableCallbackOption<string>
): undefined
export function diffChars(
  oldStr: string,
  newStr: string,
  options: DiffCharsOptionsNonabortable & NonabortableCallbackOption<string>
): undefined
export function diffChars(
  oldStr: string,
  newStr: string,
  options: DiffCharsOptionsAbortable
): ChangeObject<string>[] | undefined
export function diffChars(
  oldStr: string,
  newStr: string,
  options?: DiffCharsOptionsNonabortable
): ChangeObject<string>[]
export function diffChars(
  oldStr: string,
  newStr: string,
  options?: any
): ChangeObject<string>[] | undefined {
  if (typeof options === 'function') {
    setTimeout(() => {
      options([{value: 'bla'}]);
    }, 1000)
  } else {
    return [{value: 'bla'}];
  }
}

This was bizarrely difficult; lots of approaches that seem like they obviously logically should work just don't and I don't know why the fuck not. For example, if you swap the order of the second and third overloads, like this...

export function diffChars(
  oldStr: string,
  newStr: string,
  options: NonabortableDiffCallback<string>
): undefined;
export function diffChars(
  oldStr: string,
  newStr: string,
  options: DiffCharsOptionsNonabortable & NonabortableCallbackOption<string>
): undefined
export function diffChars(
  oldStr: string,
  newStr: string,
  options: DiffCharsOptionsAbortable & AbortableCallbackOption<string>
): undefined
export function diffChars(
  oldStr: string,
  newStr: string,
  options: DiffCharsOptionsAbortable
): ChangeObject<string>[] | undefined
export function diffChars(
  oldStr: string,
  newStr: string,
  options?: DiffCharsOptionsNonabortable
): ChangeObject<string>[]
export function diffChars(
  oldStr: string,
  newStr: string,
  options?: any
): ChangeObject<string>[] | undefined {
  if (typeof options === 'function') {
    setTimeout(() => {
      options([{value: 'bla'}]);
    }, 1000)
  } else {
    return [{value: 'bla'}];
  }
}

... then the test script no longer type checks, complaining that the final call (with timeout and callback) doesn't match any overload:

$ npx tsd

  index.test-d.ts:31:8
  ✖  29:4  No overload matches this call.
  The last overload gave the following error.
    Object literal may only specify known properties, and timeout does not exist in type DiffCharsOptionsNonabortable.  
  ✖  31:8  Parameter type ChangeObject<string>[] | undefined is declared too wide for argument type ChangeObject<string>[].                                                                                   
  ✖  34:0  Parameter type undefined is declared too wide for argument type never.                                                                                                                             

  3 errors

What the fuck, TypeScript? The overloads are literally the same, just in a different order. If an overload matched before, it should match now! How can the order of these overloads possibly matter at all when there's no way for a call to match both of them, and therefore the order in which they're checked against the call logically shouldn't be able to affect which one is chosen?! Yet it does matter - entirely incomprehensibly to me. I don't know how I could ever have known or predicted this; I just had to write comprehensive tests covering every legitimate way of calling the function, then figure out through arbitrary trial and error which ordering of overload signatures would magically make the compiler happy with all those calls. I hate this, but apparently the end result works.

Anyway, now that I've figured that out, it should be simple to fix my PR and get all the functions typed correctly.

ExplodingCabbage added a commit that referenced this issue Mar 28, 2025
ExplodingCabbage added a commit that referenced this issue Apr 28, 2025
* First steps towards TypeScript conversion

* Convert xml.js->xml.ts

* Add compilation to build step

* Rewrite base.js in TypeScript (maybe shit, need to review own work once I'm done)

* Rewrite json.js -> json.ts and get tests passing (I'm honestly shocked they passed)

* array.js -> array.ts

* character.js -> character.ts

* css.js->css.ts

* sentence.js->sentence.ts

* word.js -> word.ts

* line.js -> line.ts

* index.js->index.ts

* Convert a couple more fiiles

* Convert params.ts

* string.js -> string.ts

* Partly convert patch-related files to TypeScript; more to do

* Type more stuff

* Begin converting line-endings.js. Interesting TypeScript bug along the way.

* Bump TypeScript

* Add link to bug report

* Finish converted line-endings.js to TypeScript

* Add missing overloads to support case where argument type isn't known statically

* reverse.js -> reverse.ts

* add todo

* Rewrite base.ts types (breaks everything for now)

* Per-diff-function options types. Still not compiling but close.

* Fix build

* Run 'yarn add @eslint/js typescript-eslint --save-dev' as suggested at https://typescript-eslint.io/getting-started

* Turn on recommended eslint rules as recommended by Getting Started guide at https://typescript-eslint.io/packages/typescript-eslint/#config

* Get linting of TypeScript working (now with the officially recommended rules)

* yarn lint --fix

* Tweak some indentation that eslint broke

* Fix a couple of linting errors

* Allow explicit 'any'

I'm using it and it's convenient. In some places like diffArrays where we allow arrays of arbitrary type, I don't even know how to NOT use it.

* Eliminate needless explicit respecification of rules that eslint.configs.recommended already enables

* Fix another eslint config bug

* Start using arrow functions and thereby resolve a https://typescript-eslint.io/rules/no-this-alias/ error

I wasn't using them before because I wasn't sure if our build process would turn them into normal functions for compatability with old JS environments; it turns out it does, so we're fine.

* Fix some linting errors about pointless escape sequences

* Fix more linting errors about pointless escape sequences

* Eliminate a util made redundant by Object.keys, and fix a linting error in the process

* Fix a no-prototype-builtins linting error

* Disable no-use-before-define for TypeScript code, where it's broken

* Liberalise more rules

* Fix lint errors in parse.ts

* Fix lint errors in apply.ts

* Fix remaining linting errors

* Add missing newline at EOF

* Remove a couple of unused ts-expect-error directives. (My editor was showing errors but tsc doesn't.)

* create.js->create.ts

* Fix some linting errors

* Lint on build, like before

* Generate type declarations on build

* Migrate to using tsconfig.json instead of CLI args

* Start adding tsd tests... but actually maybe should use dtslint?

* Correctly mark a load of arguments as optional

* convert tests to tsd style

* Fix type test

* Fix some more type test things

* Undo some testing silliness

* aaaaaaaaaaargh

* Get most of the way to fixing type definitions based on #303 (comment). Doesn't compile yet.

* Fix index.ts

* Fix remaining compilation errors

* Restore 'Change' type from DefinitelyTyped

* tsd test syntax fix

* Fix: oldHeader & newHeader are allowed to be explicitly given as undefined

* Fix 'Abortable' types in patch/create

* Fix another typing error

* Fix error-handling-related types in patch/apply

* Analyse errors I'm seeing in tests copied from DT & add TODOs

* Begin adding some docs

* Fix a load of test cases where the DT behaviour was just wrong

* Make diffArrays generic instead of using any

* Make DiffArraysOptions types generic

* Rename ParsedDiff -> StructuredPatch

* Export ApplyPatchOptions, ApplyPatchesOptions

* Make all methods public for backwards compat

* Fix remaining type error

* Fix ignoring of test-d

* Copy latest change from DT (DefinitelyTyped/DefinitelyTyped@2cfdb9f)

* Flesh out docs

* Resolve ignoreWhitespace dilemma

* nah

* Remove obsolete TODOs

* Fix bad .gitignore rule

* Add another type test file

* v0.0.1

* v0.0.2

* .npmignore more stuff

* Move test-d/ out of types/ so yarn clean won't delete it

* Add attw

* durrrr

* failed fix attempt

* Get attw passing (hallelujah!)

* Enable "strict": true

* Fix a strict mode error

* Fix a strict mode error

* Fix more strict mode errors

* Silence a silly strict mode error

* Further placate strict mode

* Further placate strict mode

* Fix a legit type error that strict mode found

* Further placate strict mode

* Get TypeScript happy with create.ts

* Further placate strict mode

* Further placate strict mode

* Placate compiler about apply.ts

* Fix a load more strict mode errors

* Placate strict mode further

* Fix some more compilation errors

* Fix one more compilation error

* Fix remaining base.ts errors

* Fix line.ts

* Type a variable to remove a slew of compilation errors

* Add more types to resolve more errors

* Add even more types to resolve even more errors

* Fix remaining type errors. Huzzah!!!

* Address verbatimModuleSyntax guidance

* Remove obsolete comment

* Get @typescript-eslint/consistent-type-exports enabled

* Un-disable no-duplicate-type-constituents and let it autofix. I guess I was confused.

* Reenable another rule

* Remove no-longer-needed gitignore rule from earlier in this PR

* Remove an obsolete disabling of a rule from early in my eslint tinkering. No longer makes sense.

* Fix deps being wrongly declared as non-dev deps

* Bump new deps to latest

* Re-add support for imports from /lib/, maybe?

* Restore support for subpath imports in Node 12.19 and earlier (see #351 (comment)).

* Restore es5 support

* Improve docs; add more exports to obviate need to import stuff from lib/

* Fix rollup

* Improve docs; restore package.json top-level fields that maybe are useful for compatibility with something, somewhere

* Stop using babel with rollup.

Doesn't seem to really affect anything. Output in dist/diff.js is almost identical; just formatting changes and a handful of other things that look inconsequential.

* Remove unused dependency on Babel CLI

* Remove another unused dep

* Get karma working

* Remove apparently unneeded eslint ignore directives

* Fix misplaced bullet in release notes

* bah

* Begin the tedious work of copying and pasting the README documentation into the function comments, so it can show up in people's editors

* More copying and pasting. I should probably test this before moving on

* More copying and pasting of docs

* Finish pasting docs

* Revert temporary package name change

* Hackily resolve the Intl.Segmenter issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants