This post describes a proposal to add custom types to Slate with the following features:
Element and Text with custom propertiesLimitation:
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:
Element and Text.getChildren and getText to keep working.Element and Text in our own code without generics at each call site.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:

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.
This solves these typing issues:
@ianstormtaylor @CameronAckermanSEL @ccorcos @BrentFarese (and anybody else) would be interested in getting some feedback.
Additional thoughts:
Editor to provide custom properties. For example, in my case, I store the current user id in the editor.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.
@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.
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!
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.
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.
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.
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!
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
Cool feature, any plan on making an official release @thesunny ?
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
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.
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鈥檝e 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.
Thanks for the progress on this @thesunny and for soliciting community input.
Once you鈥檝e 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) {
~~~~~~~~~
Hi @thesunny and thanks for your work. I tested it on my code base. Two things:
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)
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 ?
@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>
@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.
Hi @thesunny and thanks for your work. I tested it on my code base. Two things:
- 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)
- 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!
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 馃憤
Most helpful comment
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
@nexttag. For example,yarn add slate@next slate-react@next slate-history@next.Once you鈥檝e 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.