-
-
Notifications
You must be signed in to change notification settings - Fork 3
The type of Children is invalid. #8
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
Comments
I decided not to create a PR yet, because this would conflict with #3. |
What about other nodes: comments, cdata, doctype, text? Leaving this vague is also nice for custom nodes. E.g., |
These are currently valid children: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/xast/index.d.ts#L54
You’re talking about the xast format, which is currently as strict as I described above. I’m talking about the fact that the types of hastscript are now incorrect. If custom types are passed as children, this also means a node is return which doesn’t match the I think this code snippet should explain the issue. The const element = h('foo', null, { type: 'custom' }) // $ExpectType Element
let foo = element.children[0].type // $ ExpectType 'element' | 'comment' | 'text' | 'instruction' | 'cdata';
console.log(foo)
// Logs 'custom'
Personally I favor the approach using stricter types. :) |
/cc @ChristianMurphy you may b better suited for this one |
Taking a step back, the question comes down to, does XAST support custom node types or not? as for
I like the idea, reusing the type from XAST, it does make me wonder if |
@remcohaszing Are you proposing for your code example to crash here?: const element = h('foo', null, { type: 'custom' }) // $ExpectType Element CRASHHH
let foo = element.children[0].type // $ ExpectType 'element' | 'comment' | 'text' | 'instruction' | 'cdata';
console.log(foo)
// Logs 'custom' Because IMO that’s fine usage: you’re explicitly injecting a custom node (which is a pretty useful feature: I haven’t seen it in xast yet but hast has As for allowing any object in Can you instead cast something on your side? (e.g., L3 in the example) |
First of all the goal of this issue: The types of children accepted by xastscript are passed directly as
Someone could cast this to a less strict type and then cast it to the actual type. This is a common trick mostly needed to avoid type errors if types are incorrect or something else is going on cause TypeScript to infer the type improperly. Typically this is undesirable. let foo = element.children[0] as any as CustomNode
let foo = element.children[0] as Node as CustomNode
I think this is a proper question to start with. If not, let’s disallow custom nodes altogether. I think the answer is yes. This means the type of Solution 1: Loosen children typesThis is the simplest solution. Simply remove the types of Solution 2: Allow registering custom xast element / root childrenModule augmentation can be used to extend modules, namespaces, and interfaces. This is typically not something recommended, but it’s ideal for this use case.
export interface ElementChildren {
element: Element;
comment: Comment;
text: Text;
instruction: Instruction;
cdata: Cdata;
}
export interface Element {
type: 'element';
children: ElementChildren[keyof ElementChildren][]
} Now if a library introduces a custom node, they can register their node as a custom child by augmenting the export interface CustomNode {
type: 'custom'
}
declare module 'xast' {
interface ElementChildren {
custom: CustomNode;
}
} If one would now use this library and import * as x from 'xastscript';
x('foo', null, { type: 'custom' });
x('foo', null, { type: 'nonexistent' }); // $ExpectError Of course the same should be use for Expanding on this thought, export interface Parent<ChildrenMap = Record<string, Node>> extends Node {
children: ChildrenMap[typeof ChildrenMap][];
} Now export interface Element extends Parent<ElementChildren> {
type: 'element';
} |
I feel like I don’t know enough about TS to have a meaningful opinion here btw, and it seems like something that’s not just related to this project, but all types for unist. |
I kinda like Solution 2, reading this again. But that’s quite a bit of work to all type packages. Is that something you could work on in the coming month? (due to unifiedjs/unified#121) |
I'd lean a bit toward solution 1, whenever possible I prefer for people to not have to modify/extend the types (except through generics). |
@ChristianMurphy I am assuming we’d also want hast to have the same solution we’d pick here. |
I agree solution 2 feels hacky, but on the other hand it feels very convenient as well. I think this fits in very well with an extensible interface such as most unist formats. I.e. the same idea is applied to The same principle could be used for example to declare https://github.com/remarkjs/remark-frontmatter/blob/main/types/index.d.ts#L50-L59 Currently yaml nodes are already in the |
😬 that makes me more uneasy than
convenient for who? 🙂
Does koa/this approach in general allow patching multiple times?
I've been recently thinking about that. Something like import { Root, BaseFlowContent, BaseContent, BaseListContent, BasePhrasingContent, BaseStaticPhrasingContent } from 'mdast'
import { ListItemGFM } from 'remark-gfm'
type plainMdast = Root
type mdastWithGFMList = Root<BaseFlowContent, BaseContent, BaseListContent | ListItemGFM, BasePhrasingContent, BaseStaticPhrasingContent> |
...sooooo, what should we do about this? 🤷♂️ |
I'm still leaning a bit towards @remcohaszing's solution 1 from #8 (comment)
I'm open to solution 2 (#8 (comment)) but my limited experience with type module patching has not been pleasant. Looking back at
it avoids type patching, but it's not a great developer experience for adding custom node types either. Which brings me back to solution 1 #8 (comment) as potentially a better alternative |
I don’t have any experience working with I think in Koa it works out great though: import Koa from 'koa'
import bodyParser from 'koa-bodyparser'
const app = new Koa()
app.use(bodyParser)
app.use(async (ctx) => {
// Defined by module augmentation in koa-bodyparser
// $ts-expect-type any
ctx.request.body;
// Defined by module augmentation in koa-bodyparser
// $ts-expect-type string
ctx.request.rawBody;
}) I think unified ASTs have some similarities to Koa context in this regard. They sort of share a global variable where no conflicts may occur (the I’ll add an example based on mdast, because it actually has plugins that define custom nodes: import * as unist from 'unist'
// @types/mdast
interface MdastNodeMap {
root: Root
paragraph: Paragraph
}
export type Node = MdastNodeMap[keyof MdastNodeMap]
export interface Parent extends unist.Parent {
children: Node[]
}
export interface Root extends Parent {
type: 'root'
}
export interface Paragraph extends Parent {
type: 'paragraph'
} // remark-frontmatter
import { Node } from 'unist'
export interface Yaml extends Node {
type: 'yaml'
value: string
}
declare module 'mdast' {
interface MdastNodeMap {
yaml: Yaml
}
} // end user code
import remark from 'remark'
import frontmatter from 'remark-frontmatter'
const mdast = remark.use(frontmatter).parse(markdown)
for (const child of children) {
if(child.type === 'yaml') {
console.log(
// $ExpectType string
child.value
)
}
}
// @ts-expect-error missing value property
const customYamlNode: Yaml = {
type: 'yaml'
} As for xastscript types: Whatever isn’t specifically processed, is added to the node children. This should be reflected by types, whether this means the xastscript return type ( I personally don’t really see a use case for extending xast types, so I’m leaning towards strictening the input. |
Especially for mdast extending node types is a thing. But also for hast (and potentially xast), |
@remcohaszing could you clarify what would be needed in |
@remcohaszing Is this now solved by the PR in DT? |
Closing as I think this is solved, but let us know if this needs to be reopened |
Subject of the issue
The type of
Children
is defined as:However, it should match the type of
xast.Element.children
, strings, numbers, andxast.Root
.Invalid child types are unprocessed, yielding an invalid xast element node, as can be seen here
Your environment
Steps to reproduce
N/A
Expected behavior
Xastscript types only allow valid children and nodes that are treated in a special way.
Actual behavior
Xastscript types allow any
unist.Node
type as a child. This is unhandled and produces an invalidxast.Element
.The text was updated successfully, but these errors were encountered: