Skip to content
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

Add Custom TypeScript Types to Slate #3725

Closed
thesunny opened this issue Jun 5, 2020 · 26 comments · Fixed by #4119 · May be fixed by #4177
Closed

Add Custom TypeScript Types to Slate #3725

thesunny opened this issue Jun 5, 2020 · 26 comments · Fixed by #4119 · May be fixed by #4177
Milestone

Comments

@thesunny
Copy link
Collaborator

thesunny commented Jun 5, 2020

This post describes a proposal to add custom types to Slate with the following features:

  • Extend types in Slate like Element and Text with custom properties
  • Does not require generics at every call site (define custom types once only)

Limitation:

  • One custom type definition per project. You can't have multiple editors in one project with different custom types. You can have multiple instances of editors.

Simple Schema Example

For simplicity, this is a model for a bare bones version of Slate's types without custom types. I use this to explain the proposal.

export type Element = {
  children: Text[]
}

export type Text = {
  text: string
}

// gets children text nodes from an element
export function getChildren(element: Element) {
  return element.children
}

// gets merged text from children text node
export function getText(element: Element) {
  return element.children.map((text) => text.text)
}

The goal:

  • We want to be able to customize Element and Text.
  • We need getChildren and getText to keep working.
  • We need to use the customized Element and Text in our own code without generics at each call site.

How To Use Custom Types

This section explains how to use Custom Types by the end user. I will then explain and show the code for how to add custom types to the schema in the sample above.

// import statement to `./slate` would actually be `slate` but I'm importing
// from a local file.
import { getChildren, Element } from "./slate"

// this is where you customize. If you omit this, it will revert to a default.
declare module "./slate" {
  export interface CustomTypes {
    Element:
      | { type: "heading"; level: number }
      | { type: "list-item"; depth: number }
    Text: { bold?: boolean; italic?: boolean }
  }
}

// This uses the custom element. It supports type discrimination.
// `element.heading` works and `element.depth` fails.
function getHeadingLevel(element: Element) {
  if (element.type !== "heading") throw new Error(`Must be a heading`)
  // Uncomment `element.depth` and you get a TypeScript error as desired
  // element.depth
  return element.level
}

// This shows that the regular methods like `getChildren` that are imported
// work as expected.
function getChildrenOfHeading(element: Element) {
  if (element.type !== "heading") throw new Error(`Must be a heading`)
  return getChildren(element)
}

Here's a screenshot showing what happens when we uncomment element.depth and that the proper typescript error shows up:

image

How It Works

Here is the source code for ./slate.ts

// This would be Element as per Slate's definition
export type BaseElement = {
  children: Text[]
}

// This would be Text as per Slate's definition
export type BaseText = {
  text: string
}

// This is the interface that end developers will extend
export interface CustomTypes {
  [key: string]: unknown
}

// prettier-ignore
export type ExtendedType<K extends string, B> =
  unknown extends CustomTypes[K]
  ? B
  : B & CustomTypes[K]

export type Text = ExtendedType<"Text", BaseText>
export type Element = ExtendedType<"Element", BaseElement>

export function getChildren(element: Element) {
  return element.children
}

export function getText(element: Element) {
  return element.children.map((text) => text.text)
}

This solution combines interface with type to get us the best of both.

An interface supports declaration merging but does not support type discrimination (ie. unions). A type supports unions and type discrimination but not declaration merging.

The solution uses the declaration merging from interface and sets its properties to a type which can be used in unions and declaration merging.

The ExtendedType takes two generic arguments. The first argument K is the key within the CustomTypes interface which the end use can extend. The second argument B is the base type to use if a custom value is not defined on CustomTypes and which is being extended.

It works by seeing if CustomTypes[K] extends unknown. If it does, it means that the custom property hasn't been added to CustomTypes. This is because unknown only extends unknown. If this is true (ie. no custom type is provided), we then make the type the Base type B (ie. Element or Text).

If it does not extend unknown, then a custom type was provided. In that case, we take the custom type at CustomTypes[K] and add it to the base type B.

Comments

This solves these typing issues:

  • Ability to extend Slate's types
  • Not having to specify types at each call site (ie. reduce type fatigue)
  • Support for type unions with type discrimination
@thesunny thesunny removed the ♥ help label Jun 6, 2020
@thesunny thesunny changed the title Proposal to add Custom Types that need to be declared only once Add Custom TypeScript Types to Slate Jun 6, 2020
@thesunny
Copy link
Collaborator Author

thesunny commented Jun 6, 2020

@ianstormtaylor @CameronAckermanSEL @ccorcos @BrentFarese (and anybody else) would be interested in getting some feedback.

Additional thoughts:

  • This may not be a breaking change
  • Can be used on Editor to provide custom properties. For example, in my case, I store the current user id in the editor.
  • This approach might need little or no refactoring.

@BrentFarese
Copy link
Collaborator

I think this is a good proposal to make declaration merging a bit better. Ultimately, I think Slate should support both generics and declaration merging but declaration merging is the "smaller lift" in terms of refactors to Slate core. So, I would be in favor of implementing something like the above somewhat soon so we all get type safety on editor.children, which is really lacking right now due to the way types are set up.

I'll let others chime in but do you want to open up a branch for the work @thesunny? Happy to contribute and collaborate on a review...

We are releasing our Slate editor somewhat soon and it'd be great to have full type safety before then. We will continue to work on the generics solution too but I think this is a good stop-gap that may provide the same type safety as generics.

@thesunny
Copy link
Collaborator Author

thesunny commented Jun 9, 2020

@BrentFarese

I was pretty excited about this proposal catching 90% of the use cases but I'm leaning towards the value of having both.

There are use cases where generics are the only solution like a method that returns the children of a Node. If the Node passed in is known, the generic can return the appropriate type. For example, TableNode could return Array<TrNode> children or Heading could return Array<Text | LinkNode> children.

One benefit of having both is that (a) they can work together and (b) you can choose the trade offs that work for you. They don't interfere with each other and generics can benefit from having custom Node, Text and other types.

I'd be happy to open a branch or @BrentFarese I'd be just as happy if you wanted to go ahead and do this yourself and cut and paste the code.

I am a little hesitant as this feels like a big decision to not involve @ianstormtaylor in before starting. I wonder if we should wait for a go ahead.

@BrentFarese
Copy link
Collaborator

I think we should go ahead @thesunny. @CameronAckermanSEL has talked to @ianstormtaylor and I think Ian is supportive of the generics option, no? As long as this isn't limiting of generics, which I don't think it is at all (it's complimentary), I think it would be good to have. We can also have it sooner than generics in my opinion. Cam or Ian can chime in but I think we go for it. Open a branch and add me as a contributor? @timbuckley from our team can contribute too. Thanks!

@thesunny
Copy link
Collaborator Author

Here's a link to the branch https://github.com/ianstormtaylor/slate/tree/declaration-merging

I haven't submitted any code. Please feel free to start without me and use the code I posted.

If you decide to go ahead, I recommend renaming CustomTypes to CustomExtensions. Originally the CustomTypes had the nodes defined in them but later I changed this so you only define the additional properties as it improves type safety; however, I did not update the name to reflect that.

@BrentFarese
Copy link
Collaborator

Just FYI some fellows from MLH are working on implementing this. We can share a branch soon. I think the issue of not being able to have 2 different sets of types for multiple editors is acceptable because a user that has multiple editors can just include custom types for all editors the user might have, which is better than the current type system. It's not perfect, but definitely better than the current state.

@BrentFarese
Copy link
Collaborator

Here is the branch where the work is occurring. https://github.com/arity-contracts/slate/tree/custom-extensions. If anyone wants to be added to help out, let us know but I think we have enough resources to complete this.

@mdmjg
Copy link
Contributor

mdmjg commented Jul 13, 2020

Here is the branch where the work is occurring. https://github.com/arity-contracts/slate/tree/custom-extensions. If anyone wants to be added to help out, let us know but I think we have enough resources to complete this.

Actually due to some issues the branch is now https://github.com/arity-contracts/slate/tree/custom-types

Sorry!

@BrentFarese BrentFarese added this to the Slate 1.0 milestone Nov 3, 2020
thesunny added a commit that referenced this issue Nov 24, 2020
This PR adds better TypeScript types into Slate and is based on the proposal here: #3725

* Extend Slate's types like Element and Text

* Supports type discrimination (ie. if an element has type === "table" then we get a reduced set of properties)

* added custom types

* files

* more extensions

* files

* changed fixtures

* changes eslint file

* changed element.children to descendant

* updated types

* more type changes

* changed a lot of typing, still getting building errors

* extended text type in slate-react

* removed type assertions

* Clean up of custom types and a couple uneeded comments.

* Rename headingElement-true.tsx.tsx to headingElement-true.tsx

* moved basetext and baselement

* Update packages/slate/src/interfaces/text.ts

Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>

* Fix some type issues with core functions.

* Clean up text and element files.

* Convert other types to extended types.

* Change the type of editor.marks to the appropriate type.

* Add version 100.0.0 to package.json

* Revert "Add version 100.0.0 to package.json"

This reverts commit 329e44e.

* added custom types

* files

* more extensions

* files

* changed fixtures

* changes eslint file

* changed element.children to descendant

* updated types

* more type changes

* changed a lot of typing, still getting building errors

* extended text type in slate-react

* removed type assertions

* Clean up of custom types and a couple uneeded comments.

* Rename headingElement-true.tsx.tsx to headingElement-true.tsx

* moved basetext and baselement

* Update packages/slate/src/interfaces/text.ts

Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>

* Fix some type issues with core functions.

* Clean up text and element files.

* Convert other types to extended types.

* Change the type of editor.marks to the appropriate type.

* Run linter.

* Remove key:string uknown from the base types.

* Clean up types after removing key:string unknown.

* Lint and prettier fixes.

* Implement custom-types

Co-authored-by: mdmjg <mdj308@nyu.edu>

* added custom types to examples

* reset yarn lock

* added ts to fixtures

* examples custom types

* Working fix

* ts-thesunny-try

* Extract interface types.

* Fix minor return type in create-editor.

* Fix the typing issue with Location having compile time CustomTypes

* Extract types for Transforms.

* Update README.

* Fix dependency on slate-history in slate-react

Co-authored-by: mdmjg <mdj308@nyu.edu>
Co-authored-by: Brent Farese <brentfarese@gmail.com>
Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>
Co-authored-by: Tim Buckley <timothypbuckley@gmail.com>
@thesunny
Copy link
Collaborator Author

This is now available in the @next version of slate and slate-react.

To install, use yarn add slate@next slate-react@next or npm install slate@next slate-react@next

@htulipe
Copy link
Contributor

htulipe commented Nov 25, 2020

Cool feature, any plan on making an official release @thesunny ?

@thesunny
Copy link
Collaborator Author

thesunny commented Dec 1, 2020

IMPORTANT! Due to an issue when switching from the master branch to the main branch as the default branch when accepting the related PR, the Slate Types are currently not part of the @next release.

I've reopened this issue.

For more details, please refer to #4003

@thesunny thesunny reopened this Dec 1, 2020
@thesunny
Copy link
Collaborator Author

This is now available in the @next release.

Use yarn add slate@next or the npm equivalent. You should probably also use yarn add slate-react@next and yarn add slate-history@next.

@thesunny
Copy link
Collaborator Author

Hello everyone, we could use some help by having people try out the new TypeScript types in their own editors. They are available by using an @next tag. For example, yarn add slate@next slate-react@next slate-history@next.

Once you’ve integrated it, please let us know by posting below.

We want to get this into the regular npm packages as quickly as possible. This has the benefit of getting better types into Slate, but also has the added benefit of being able to start accepting more PRs. Ian has added more maintainers and given out more publishing rights so this should allow us to accept more PRs faster into Slate.

@vpontis
Copy link
Contributor

vpontis commented Dec 15, 2020

Thanks for the progress on this @thesunny and for soliciting community input.

Once you’ve integrated it, please let us know by posting below.

I'm testing these types in Luma. You can see an interactive version of our editor here: https://lu.ma/play-editor

I am getting a lot of TypeScript errors while testing this new release.

I have updated my types file to this gist: https://gist.github.com/vpontis/fc783570a04d2d9e14c748002f7d3ae2

Here is one of my components:

export const SlateLeaf = ({ attributes, children, leaf }: RenderLeafProps) => {
  if (leaf.bold) {
    children = <strong>{children}</strong>;
  }

  if (leaf.code) {
    children = <code>{children}</code>;
  }

  if (leaf.italic) {
    children = <em>{children}</em>;
  }

  if (leaf.underline) {
    children = <u>{children}</u>;
  }

  return <span {...attributes}>{children}</span>;
};

Then, when compiling the TS:

components/rich-text/rich-render.tsx:73:12 - error TS2339: Property 'bold' does not exist on type 'BaseText'.

73   if (leaf.bold) {
              ~~~~

components/rich-text/rich-render.tsx:77:12 - error TS2339: Property 'code' does not exist on type 'BaseText'.

77   if (leaf.code) {
              ~~~~

components/rich-text/rich-render.tsx:81:12 - error TS2339: Property 'italic' does not exist on type 'BaseText'.

81   if (leaf.italic) {
              ~~~~~~

components/rich-text/rich-render.tsx:85:12 - error TS2339: Property 'underline' does not exist on type 'BaseText'.

85   if (leaf.underline) {
              ~~~~~~~~~

@htulipe
Copy link
Contributor

htulipe commented Dec 15, 2020

Hi @thesunny and thanks for your work. I tested it on my code base. Two things:

  1. TS Error
Transforms.insertNodes(
          editor,
          {
            type: "paragraph",
            children: [
              {
                text: "",
              },
            ],
          }
        );

I get an error much like @vpontis saying:

Argument of type '{ type: string; children: { text: string; }[]; }' is not assignable to parameter of type 'BaseElement | BaseText | BaseEditor | BaseNode[]'.
  Object literal may only specify known properties, and 'type' does not exist in type 'BaseElement | BaseText | BaseEditor | BaseNode[]'.ts(2345)
  1. Custom type modelisation question:
    You gave this example:
export interface CustomTypes {
    Element:
      | { type: "heading"; level: number }
      | { type: "list-item"; depth: number }
    Text: { bold?: boolean; italic?: boolean }
  }

where one can see that there is no need to say that heading or list-item have a children property. I understand that your code "injects" that prop automatically. That works fine but my codebase needs a TS interface to the whole definition for each element. Using the main slate release I have something like that:

interface HeadingElement extends Element {
   type: "heading",
   level: number
}

I managed to achieve the same thing with your TS definition:

interface HeadingElement extends BaseElement {
   type: "heading",
   level: number
}

then I override CustomTypes:

export interface CustomTypes {
    Element:
      | HeadingSlateElement
      | LinkSlateElement
...

Is this the "correct" way ?

@vpontis
Copy link
Contributor

vpontis commented Dec 15, 2020

@thesunny I'm surprised by the way that we are extending types. I haven't seen any other package require you to do declare module to create custom types.

Would it make sense to do an API like this:

const editor = createEditor<CustomElement, CustomLeaf>();

type CustomRenderLeafProps = RenderLeafProps<Override>

@thesunny
Copy link
Collaborator Author

@htulipe @vpontis 

For the record and to give credits where it's due @BrentFarese, @timbuckley @mdmjg did the hard work. I did the initial design and helped a bit at the end.

I @mentioned them to get them into this conversation. I think they will do a better job at answering any questions. I haven't integrated the new types in my app at the moment.

@BrentFarese
Copy link
Collaborator

BrentFarese commented Dec 15, 2020

Hi @thesunny and thanks for your work. I tested it on my code base. Two things:

  1. TS Error
Transforms.insertNodes(
          editor,
          {
            type: "paragraph",
            children: [
              {
                text: "",
              },
            ],
          }
        );

I get an error much like @vpontis saying:

Argument of type '{ type: string; children: { text: string; }[]; }' is not assignable to parameter of type 'BaseElement | BaseText | BaseEditor | BaseNode[]'.
  Object literal may only specify known properties, and 'type' does not exist in type 'BaseElement | BaseText | BaseEditor | BaseNode[]'.ts(2345)
  1. Custom type modelisation question:
    You gave this example:
export interface CustomTypes {
    Element:
      | { type: "heading"; level: number }
      | { type: "list-item"; depth: number }
    Text: { bold?: boolean; italic?: boolean }
  }

where one can see that there is no need to say that heading or list-item have a children property. I understand that your code "injects" that prop automatically. That works fine but my codebase needs a TS interface to the whole definition for each element. Using the main slate release I have something like that:

interface HeadingElement extends Element {
   type: "heading",
   level: number
}

I managed to achieve the same thing with your TS definition:

interface HeadingElement extends BaseElement {
   type: "heading",
   level: number
}

then I override CustomTypes:

export interface CustomTypes {
    Element:
      | HeadingSlateElement
      | LinkSlateElement
...

Is this the "correct" way ?

Yes this is the "correct" way to extend the types.

I notice in your custom types definition, it doesn't seem like you have defined paragraph. Have you defined paragraph in your types @htulipe?

Also I would encourage this discussion on the #typescript channel in Slate Slack vs. here on GitHub. Thanks!

@htulipe
Copy link
Contributor

htulipe commented Dec 16, 2020

Hi @BrentFarese, it's only a excerpt of my code, I do have paragraph defined. I'll ask about my TS error on the Slack channel 👍

@williamstein
Copy link

This is just a little feedback from a new user of this PR...

As a relatively new users I was a little confused when switching to @next, since all of the examples in site/examples have a large number of Typescript errors with this @next version of slate. I decided to use @next, since I didn't want to just have to invent my own way to get better typing. Immediately nothing works at all (compared to the 0.59), in the sense that typescript produces a large number of (non-fatal) errors. I thought: oh, I'll just check the examples in site/examples, since presumably they have been updated to use whatever the new not-in-the-docs method is for declaring types. However, that didn't work for me, since the examples don't "work" either (they run of course, but produces a lot of typescript errors). Finally, after stumbling around the source code for a while and using git blame, I discovered this PR, and the discussion above explains pretty clearly exactly how to use the new types.

Anyway, the above didn't take too long, and now that I understand how this PR works, I really like it (THANKS!). But definitely a blocker on the todo list for releasing the next version of slate should be to fix the examples, and update the docs, or find a way to make this explicit typing optional and turn that on by default. I'm not volunteering, and again I greatly appreciate all the work you have already done on this!

@macrozone
Copy link
Contributor

macrozone commented Jan 21, 2021

i am testing the new version for the upcoming major update of react-page (see https://github.com/react-page/react-page/tree/beta)

i have to yet find out if the declare module approach works for it, but i agree with #3725 (comment) that its probably not the way to go and generics are more suitable for the job.

Luckely for our library, it will be hidden for the consumer of our library, thats what counts.

keep you updated with feedback

Edit 1:

it does not seem to work for Transforms.setNodes and Transforms.wrapNodes. I have to do an any (or unknown) cast there, or it won't accept my arguments

Work in progress: react-page/react-page#890

i had to fallback to any casts, i could not figure out how to do it problerly, maybe someone can help?

@michael-land
Copy link

michael-land commented Jan 27, 2021

@thesunny The docs and src code says that the transformer types is

Transforms.wrapNodes(editor: Editor, element: Element, options?)

However, the node_modules/slate shows all transform methods taks BaseEditor, BaseElement, BaseText type.

export declare const Transforms: {
    delete: (editor: import("..").BaseEditor, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        distance?: number | undefined;
        unit?: "character" | "word" | "line" | "block" | undefined;
        reverse?: boolean | undefined;
        hanging?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    insertFragment: (editor: import("..").BaseEditor, fragment: import("..").BaseNode[], options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        hanging?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    insertText: (editor: import("..").BaseEditor, text: string, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    collapse: (editor: import("..").BaseEditor, options?: {
        edge?: "anchor" | "focus" | "start" | "end" | undefined;
    } | undefined) => void;
    deselect: (editor: import("..").BaseEditor) => void;
    move: (editor: import("..").BaseEditor, options?: {
        distance?: number | undefined;
        unit?: "character" | "word" | "line" | "offset" | undefined;
        reverse?: boolean | undefined;
        edge?: "anchor" | "focus" | "start" | "end" | undefined;
    } | undefined) => void;
    select: (editor: import("..").BaseEditor, target: import("..").Location) => void;
    setPoint: (editor: import("..").BaseEditor, props: Partial<import("..").BasePoint>, options?: {
        edge?: "anchor" | "focus" | "start" | "end" | undefined;
    } | undefined) => void;
    setSelection: (editor: import("..").BaseEditor, props: Partial<import("..").BaseRange>) => void;
    insertNodes: (editor: import("..").BaseEditor, nodes: import("..").BaseEditor | import("..").BaseElement | import("..").BaseText | import("..").BaseNode[], options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | undefined;
        hanging?: boolean | undefined;
        select?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    liftNodes: (editor: import("..").BaseEditor, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | "all" | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    mergeNodes: (editor: import("..").BaseEditor, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | undefined;
        hanging?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    moveNodes: (editor: import("..").BaseEditor, options: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | "all" | undefined;
        to: import("..").Path;
        voids?: boolean | undefined;
    }) => void;
    removeNodes: (editor: import("..").BaseEditor, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | undefined;
        hanging?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    setNodes: (editor: import("..").BaseEditor, props: Partial<import("..").BaseEditor> | Partial<import("..").BaseElement> | Partial<import("..").BaseText>, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | "all" | undefined;
        hanging?: boolean | undefined;
        split?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    splitNodes: (editor: import("..").BaseEditor, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | undefined;
        always?: boolean | undefined;
        height?: number | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    unsetNodes: (editor: import("..").BaseEditor, props: string | string[], options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | "all" | undefined;
        split?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    unwrapNodes: (editor: import("..").BaseEditor, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | "all" | undefined;
        split?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    wrapNodes: (editor: import("..").BaseEditor, element: import("..").BaseElement, options?: {
        at?: import("..").Path | import("..").BasePoint | import("..").BaseRange | undefined;
        match?: ((node: import("..").BaseNode) => boolean) | undefined;
        mode?: "highest" | "lowest" | "all" | undefined;
        split?: boolean | undefined;
        voids?: boolean | undefined;
    } | undefined) => void;
    transform: (editor: import("..").BaseEditor, op: import("..").Operation) => void;
};
//# sourceMappingURL=index.d.ts.map

If I import NodeTransforms from slate/dist/transforms/node directly, it has the correct type. Any ideas why the re-export change the method type signature?

@macrozone
Copy link
Contributor

macrozone commented Jan 28, 2021

@thesunny The docs and src code says that the transformer types is

Transforms.wrapNodes(editor: Editor, element: Element, options?)

However, the node_modules/slate shows all transform methods taks BaseEditor, BaseElement, BaseText type.

.....

If I import NodeTransforms from slate/dist/transforms/node directly, it has the correct type. Any ideas why the re-export change the method type signature?

i think i am running in the same problem.

on a side note: how can i do this declaration once in my project? i had to repeat the declaration in any file.

lukesmurray pushed a commit to lukesmurray/slate that referenced this issue Feb 5, 2021
This PR adds better TypeScript types into Slate and is based on the proposal here: ianstormtaylor#3725

* Extend Slate's types like Element and Text

* Supports type discrimination (ie. if an element has type === "table" then we get a reduced set of properties)

* added custom types

* files

* more extensions

* files

* changed fixtures

* changes eslint file

* changed element.children to descendant

* updated types

* more type changes

* changed a lot of typing, still getting building errors

* extended text type in slate-react

* removed type assertions

* Clean up of custom types and a couple uneeded comments.

* Rename headingElement-true.tsx.tsx to headingElement-true.tsx

* moved basetext and baselement

* Update packages/slate/src/interfaces/text.ts

Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>

* Fix some type issues with core functions.

* Clean up text and element files.

* Convert other types to extended types.

* Change the type of editor.marks to the appropriate type.

* Add version 100.0.0 to package.json

* Revert "Add version 100.0.0 to package.json"

This reverts commit 329e44e.

* added custom types

* files

* more extensions

* files

* changed fixtures

* changes eslint file

* changed element.children to descendant

* updated types

* more type changes

* changed a lot of typing, still getting building errors

* extended text type in slate-react

* removed type assertions

* Clean up of custom types and a couple uneeded comments.

* Rename headingElement-true.tsx.tsx to headingElement-true.tsx

* moved basetext and baselement

* Update packages/slate/src/interfaces/text.ts

Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>

* Fix some type issues with core functions.

* Clean up text and element files.

* Convert other types to extended types.

* Change the type of editor.marks to the appropriate type.

* Run linter.

* Remove key:string uknown from the base types.

* Clean up types after removing key:string unknown.

* Lint and prettier fixes.

* Implement custom-types

Co-authored-by: mdmjg <mdj308@nyu.edu>

* added custom types to examples

* reset yarn lock

* added ts to fixtures

* examples custom types

* Working fix

* ts-thesunny-try

* Extract interface types.

* Fix minor return type in create-editor.

* Fix the typing issue with Location having compile time CustomTypes

* Extract types for Transforms.

* Update README.

* Fix dependency on slate-history in slate-react

Co-authored-by: mdmjg <mdj308@nyu.edu>
Co-authored-by: Brent Farese <brentfarese@gmail.com>
Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>
Co-authored-by: Tim Buckley <timothypbuckley@gmail.com>
@thesunny
Copy link
Collaborator Author

@vpontis @macrozone 

Answering some questions/addressing concerns:

One of the reasons for this unusual way of adding types is so that developers can define custom types once and it should propagate through all of Slate's method calls. If we used generics at call sites, the type has to be passed in to every Slate function call which can be cumbersome. Note that this proposal should be compatible with a generics based version so in the future if somebody wants to contribute that, they can still do so.

You could argue that all other things being equal, generics should have been done first; however, another factor is that this was much easier to complete and for many people, some stricter typing was better than nothing. The alternative to this would probably be no custom types.

It does appear that the documentation is out of date as you mentioned. That's probably a good reason to keep it in the "@next" release until the docs are improved. We would love contributions to improve this. ❤️

In the repo, you'll find /site/examples/custom-types.d.ts and that is declared once and used by all the files in the /site/examples directory. You should be able to do something similar.

@thesunny
Copy link
Collaborator Author

Hmm... I'm playing around with the types and it seems like some of the things are broken as the custom types don't appear to be passed through automatically in all the method calls.

I will try and sort this out with the other contributors.

@jasonphillips
Copy link
Contributor

jasonphillips commented Feb 25, 2021

Small note on this:

@thesunny I'm surprised by the way that we are extending types. I haven't seen any other package require you to do declare module to create custom types.

Would it make sense to do an API like this:

const editor = createEditor<CustomElement, CustomLeaf>();

type CustomRenderLeafProps = RenderLeafProps<Override>

I agree, and find that the current declarations approach will run into serious limitations in our project, where we have multiple variations on a Slate editor in the same app with different features enabled for different contexts--which runs quickly into difficulties when required to use a global override (as was already acknowledged in the "limitations" above, I realize).

EDIT: Adding onto this idea, it seems to me that one of the great advantages of the current Slate is the clean way of extending the editor with withSomeFeature wrappers. These work well with types when it comes to returning an editor object with new capabilities (eg. function withMyFeature<T extends Editor>(editor: T): T & MyFeatureEditor { ... }), so it would be ideal for this wrapping to have a way to easily extend the types used by core methods at the same time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants