question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add Custom TypeScript Types to Slate

See original GitHub issue

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

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:20
  • Comments:26 (9 by maintainers)

github_iconTop GitHub Comments

8reactions
vpontiscommented, 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>
4reactions
jasonphillipscommented, 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

TypeScript - Slate
TypeScript. Slate supports typing of one Slate document model (ie. one set of custom Editor , Element and Text types). If you need...
Read more >
@types/slate-react - npm
Stub TypeScript definitions entry for slate-react, which provides its own types definitions. Latest version: 0.50.1, last published: a year ...
Read more >
Typescript - Plate
The core types will be the default of Slate in the near future, see this PR. ... is the text node of the...
Read more >
How to add custom types into the TypeScript project - drag13.io
Create custom typings · Create a root folder for your types. You can give any name for it but let it be types...
Read more >
Adding Custom Styles in Slate.js - YouTube
Learn how to add custom styles to the Slate.js editor.Code: https://github.com/benawad/mini-google-docs-clone/tree/5_stylingPlaylist: ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found