diff --git a/package-lock.json b/package-lock.json index 3f32f9633fdf..d3659ca59d5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "source-map-support": "^0.5.21", "tiny-glob": "^0.2.9", "tslib": "^2.5.0", - "typescript": "^3.7.5", + "typescript": "^3.9.10", "util": "^0.12.5" }, "engines": { diff --git a/package.json b/package.json index 25aafb817a6c..eeaa20bfdd53 100644 --- a/package.json +++ b/package.json @@ -90,9 +90,10 @@ }, "types": "types/runtime/index.d.ts", "scripts": { - "test": "npm run test:unit && npm run test:integration", + "test": "npm run test:unit && npm run test:integration && npm run test:types", "test:integration": "mocha --exit", "test:unit": "mocha --config .mocharc.unit.js --exit", + "test:types": "tsc --project test/types/tsconfig.json --noEmit", "quicktest": "mocha --exit", "build": "rollup -c && npm run tsd", "prepare": "node scripts/skip_in_ci.js npm run build", @@ -157,7 +158,7 @@ "source-map-support": "^0.5.21", "tiny-glob": "^0.2.9", "tslib": "^2.5.0", - "typescript": "^3.7.5", + "typescript": "^3.9.10", "util": "^0.12.5" } } diff --git a/src/compiler/compile/render_dom/invalidate.ts b/src/compiler/compile/render_dom/invalidate.ts index ffc5b6b42e44..483f52e830f6 100644 --- a/src/compiler/compile/render_dom/invalidate.ts +++ b/src/compiler/compile/render_dom/invalidate.ts @@ -50,6 +50,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names: const extra_args = tail.map(variable => get_invalidated(variable)).filter(Boolean); if (is_store_value) { + // TODO: check why there are 4 parameters, but `set_store_value` only expects 3 return x`@set_store_value(${head.name.slice(1)}, ${node}, ${head.name}, ${extra_args})`; } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 4ffa9e47425e..ffcc8cad4736 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -1,5 +1,32 @@ import { ResizeObserverSingleton } from './ResizeObserverSingleton'; import { contenteditable_truthy_values, has_prop } from './utils'; +import { Fragment } from './types.js'; + +// marks a part in the code where types +// 1. should be improved or +// 2. where casting is needed in order to satisfy TypeScript +// a deeper look at these parts is needed to check if they can be replaced with a normal cast or if they currently contain a potential bug +type TODO = T + +type NodeEx = Node & { + claim_order?: number, + hydrate_init?: true, + actual_end_child?: NodeEx, + childNodes: NodeListOf, +} + +type HTMLInputElementEx = HTMLInputElement & { + __value: string +} + +type HTMLSelectElementEx = HTMLSelectElement & { + __value: string + options: HTMLOptionElementEx[] +} + +type HTMLOptionElementEx = HTMLOptionElement & { + __value: string +} // Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM // at the end of hydration without touching the remaining nodes. @@ -12,13 +39,6 @@ export function end_hydrating() { is_hydrating = false; } -type NodeEx = Node & { - claim_order?: number, - hydrate_init?: true, - actual_end_child?: NodeEx, - childNodes: NodeListOf, -}; - function upper_bound(low: number, high: number, key: (index: number) => number, value: number) { // Return first index of value larger than input value in the range [low, high) while (low < high) { @@ -152,7 +172,7 @@ export function get_root_for_style(node: Node): ShadowRoot | Document { if (root && (root as ShadowRoot).host) { return root as ShadowRoot; } - return node.ownerDocument; + return node.ownerDocument as TODO; } export function append_empty_stylesheet(node: Node) { @@ -171,12 +191,12 @@ export function append_hydration(target: NodeEx, node: NodeEx) { init_hydrate(target); if ((target.actual_end_child === undefined) || ((target.actual_end_child !== null) && (target.actual_end_child.parentNode !== target))) { - target.actual_end_child = target.firstChild; + target.actual_end_child = target.firstChild as TODO; } // Skip nodes of undefined ordering - while ((target.actual_end_child !== null) && (target.actual_end_child.claim_order === undefined)) { - target.actual_end_child = target.actual_end_child.nextSibling; + while ((target.actual_end_child !== null) && ((target.actual_end_child as NodeEx).claim_order === undefined)) { + target.actual_end_child = (target.actual_end_child as NodeEx).nextSibling as TODO; } if (node !== target.actual_end_child) { @@ -185,7 +205,7 @@ export function append_hydration(target: NodeEx, node: NodeEx) { target.insertBefore(node, target.actual_end_child); } } else { - target.actual_end_child = node.nextSibling; + target.actual_end_child = node.nextSibling as ChildNode | undefined; } } else if (node.parentNode !== target || node.nextSibling !== null) { target.appendChild(node); @@ -206,11 +226,11 @@ export function insert_hydration(target: NodeEx, node: NodeEx, anchor?: NodeEx) export function detach(node: Node) { if (node.parentNode) { - node.parentNode.removeChild(node); + (node.parentNode as TODO).removeChild(node); } } -export function destroy_each(iterations, detaching) { +export function destroy_each(iterations: Fragment[], detaching: 0 | 1) { for (let i = 0; i < iterations.length; i += 1) { if (iterations[i]) iterations[i].d(detaching); } @@ -239,7 +259,7 @@ export function object_without_properties(obj: T, exclude: return target; } -export function svg_element(name: K): SVGElement { +export function svg_element(name: K): SVGElementTagNameMap[K] { return document.createElementNS('http://www.w3.org/2000/svg', name); } @@ -264,40 +284,37 @@ export function listen(node: EventTarget, event: string, handler: EventListenerO return () => node.removeEventListener(event, handler, options); } -export function prevent_default(fn) { - return function (event) { +export function prevent_default(fn: (event: E) => void) { + return function(this: unknown, event: E) { event.preventDefault(); - // @ts-ignore return fn.call(this, event); }; } -export function stop_propagation(fn) { - return function (event) { +export function stop_propagation(fn: (event: E) => void) { + return function(this: unknown, event: E) { event.stopPropagation(); - // @ts-ignore return fn.call(this, event); }; } -export function stop_immediate_propagation(fn) { - return function (event) { +export function stop_immediate_propagation(fn: (event: E) => void) { + return function (this: unknown, event: E) { event.stopImmediatePropagation(); // @ts-ignore return fn.call(this, event); }; } -export function self(fn) { - return function (event) { +export function self(fn: (event: E) => void) { + return function(this: unknown, event: E) { // @ts-ignore if (event.target === this) fn.call(this, event); }; } -export function trusted(fn) { - return function (event) { - // @ts-ignore +export function trusted(fn: (event: E) => void) { + return function(this: unknown, event: E) { if (event.isTrusted) fn.call(this, event); }; } @@ -323,13 +340,13 @@ export function set_attributes(node: Element & ElementCSSInlineStyle, attributes if (attributes[key] == null) { node.removeAttribute(key); } else if (key === 'style') { - node.style.cssText = attributes[key]; + node.style.cssText = attributes[key] as unknown as TODO; } else if (key === '__value') { (node as any).value = node[key] = attributes[key]; } else if (descriptors[key] && descriptors[key].set && always_set_through_set_attribute.indexOf(key) === -1) { node[key] = attributes[key]; } else { - attr(node, key, attributes[key]); + attr(node, key, attributes[key] as unknown as TODO); } } } @@ -340,15 +357,15 @@ export function set_svg_attributes(node: Element & ElementCSSInlineStyle, attrib } } -export function set_custom_element_data_map(node, data_map: Record) { +export function set_custom_element_data_map(node: Element, data_map: Record) { Object.keys(data_map).forEach((key) => { - set_custom_element_data(node, key, data_map[key]); + set_custom_element_data(node, key, data_map[key] as TODO); }); } -export function set_custom_element_data(node, prop, value) { +export function set_custom_element_data(node: Element, prop: string, value: string) { if (prop in node) { - node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value; + node[prop] = typeof node[(prop as keyof typeof node)] === 'boolean' && value === '' ? true : value; } else { attr(node, prop, value); } @@ -358,11 +375,11 @@ export function set_dynamic_element_data(tag: string) { return (/-/.test(tag)) ? set_custom_element_data_map : set_attributes; } -export function xlink_attr(node, attribute, value) { +export function xlink_attr(node: Element, attribute: string, value: string) { node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value); } -export function get_binding_group_value(group, __value, checked) { +export function get_binding_group_value(group: HTMLInputElementEx[], __value: string, checked: boolean) { const value = new Set(); for (let i = 0; i < group.length; i += 1) { if (group[i].checked) value.add(group[i].__value); @@ -420,11 +437,11 @@ export function init_binding_group_dynamic(group, indexes: number[]) { }; } -export function to_number(value) { +export function to_number(value: string | number) { return value === '' ? null : +value; } -export function time_ranges_to_array(ranges) { +export function time_ranges_to_array(ranges: TimeRanges) { const array = []; for (let i = 0; i < ranges.length; i += 1) { array.push({ start: ranges.start(i), end: ranges.end(i) }); @@ -435,17 +452,19 @@ export function time_ranges_to_array(ranges) { type ChildNodeEx = ChildNode & NodeEx; type ChildNodeArray = ChildNodeEx[] & { - claim_info?: { - /** - * The index of the last claimed element - */ - last_index: number; - /** - * The total number of elements claimed - */ - total_claimed: number; - } -}; + claim_info?: ClaimInfo +} + +type ClaimInfo = { + /** + * The index of the last claimed element + */ + last_index: number; + /** + * The total number of elements claimed + */ + total_claimed: number; +} export function children(element: Element) { return Array.from(element.childNodes); @@ -457,13 +476,13 @@ function init_claim_info(nodes: ChildNodeArray) { } } -function claim_node(nodes: ChildNodeArray, predicate: (node: ChildNodeEx) => node is R, processNode: (node: ChildNodeEx) => ChildNodeEx | undefined, createNode: () => R, dontUpdateLastIndex: boolean = false) { +function claim_node(nodes: ChildNodeArray, predicate: (node: ChildNodeEx) => node is R, processNode: (node: R) => R | undefined, createNode: () => R, dontUpdateLastIndex: boolean = false) { // Try to find nodes in an order such that we lengthen the longest increasing subsequence init_claim_info(nodes); const resultNode = (() => { // We first try to find an element after the previous one - for (let i = nodes.claim_info.last_index; i < nodes.length; i++) { + for (let i = (nodes.claim_info as TODO).last_index; i < nodes.length; i++) { const node = nodes[i]; if (predicate(node)) { @@ -475,7 +494,7 @@ function claim_node(nodes: ChildNodeArray, predicate: (no nodes[i] = replacement; } if (!dontUpdateLastIndex) { - nodes.claim_info.last_index = i; + (nodes.claim_info as TODO).last_index = i; } return node; } @@ -484,7 +503,7 @@ function claim_node(nodes: ChildNodeArray, predicate: (no // Otherwise, we try to find one before // We iterate in reverse so that we don't go too far back - for (let i = nodes.claim_info.last_index - 1; i >= 0; i--) { + for (let i = (nodes.claim_info as TODO).last_index - 1; i >= 0; i--) { const node = nodes[i]; if (predicate(node)) { @@ -496,10 +515,10 @@ function claim_node(nodes: ChildNodeArray, predicate: (no nodes[i] = replacement; } if (!dontUpdateLastIndex) { - nodes.claim_info.last_index = i; + (nodes.claim_info as TODO).last_index = i; } else if (replacement === undefined) { // Since we spliced before the last_index, we decrease it - nodes.claim_info.last_index--; + (nodes.claim_info as TODO).last_index--; } return node; } @@ -509,16 +528,35 @@ function claim_node(nodes: ChildNodeArray, predicate: (no return createNode(); })(); - resultNode.claim_order = nodes.claim_info.total_claimed; - nodes.claim_info.total_claimed += 1; + resultNode.claim_order = (nodes.claim_info as TODO).total_claimed; + (nodes.claim_info as TODO).total_claimed += 1; return resultNode; } -function claim_element_base(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }, create_element: (name: string) => Element | SVGElement) { - return claim_node( +function claim_element_base( + nodes: ChildNodeArray, + name: Name, + attributes: { [key: string]: boolean }, + create_element: (name: Name) => Map[Name] +): Map[Name] + +function claim_element_base( + nodes: ChildNodeArray, + name: Name, + attributes: { [key: string]: boolean }, + create_element: (name: Name) => Map[Name] +): Map[Name] + +function claim_element_base( + nodes: ChildNodeArray, + name: Name, + attributes: { [key: string]: boolean }, + create_element: (name: Name) => Map[Name] +) { + return claim_node( nodes, - (node: ChildNode): node is Element | SVGElement => node.nodeName === name, - (node: Element) => { + (node): node is Map[Name] => node.nodeName === name, + (node) => { const remove = []; for (let j = 0; j < node.attributes.length; j++) { const attribute = node.attributes[j]; @@ -533,19 +571,19 @@ function claim_element_base(nodes: ChildNodeArray, name: string, attributes: { [ ); } -export function claim_element(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }) { +export function claim_element(nodes: ChildNodeArray, name: N, attributes: { [key: string]: boolean }) { return claim_element_base(nodes, name, attributes, element); } -export function claim_svg_element(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }) { +export function claim_svg_element(nodes: ChildNodeArray, name: N, attributes: { [key: string]: boolean }) { return claim_element_base(nodes, name, attributes, svg_element); } -export function claim_text(nodes: ChildNodeArray, data) { +export function claim_text(nodes: ChildNodeArray, data: string) { return claim_node( nodes, - (node: ChildNode): node is Text => node.nodeType === 3, - (node: Text) => { + (node): node is Text => node.nodeType === 3, + (node) => { const dataStr = '' + data; if (node.data.startsWith(dataStr)) { if (node.data.length !== dataStr.length) { @@ -560,7 +598,7 @@ export function claim_text(nodes: ChildNodeArray, data) { ); } -export function claim_space(nodes) { +export function claim_space(nodes: ChildNodeArray) { return claim_text(nodes, ' '); } @@ -577,23 +615,23 @@ export function claim_comment(nodes:ChildNodeArray, data) { ); } -function find_comment(nodes, text, start) { +function find_comment(nodes: ChildNodeArray, text: string, start: number) { for (let i = start; i < nodes.length; i += 1) { const node = nodes[i]; - if (node.nodeType === 8 /* comment node */ && node.textContent.trim() === text) { + if (node.nodeType === 8 /* comment node */ && (node.textContent as TODO).trim() === text) { return i; } } return nodes.length; } - -export function claim_html_tag(nodes, is_svg: boolean) { +export function claim_html_tag(nodes: ChildNodeArray, is_svg: boolean) { // find html opening tag const start_index = find_comment(nodes, 'HTML_TAG_START', 0); const end_index = find_comment(nodes, 'HTML_TAG_END', start_index); if (start_index === end_index) { - return new HtmlTagHydration(undefined, is_svg); + // @ts-expect-error `HtmlTagHydration` does not expect a second parameter + return new HtmlTagHydration(undefined, is_svg); } init_claim_info(nodes); @@ -602,9 +640,10 @@ export function claim_html_tag(nodes, is_svg: boolean) { detach(html_tag_nodes[html_tag_nodes.length - 1]); const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1); for (const n of claimed_nodes) { - n.claim_order = nodes.claim_info.total_claimed; - nodes.claim_info.total_claimed += 1; + n.claim_order = (nodes.claim_info as TODO).total_claimed; + (nodes.claim_info as TODO).total_claimed += 1; } + // @ts-expect-error `HtmlTagHydration` does not expect a second parameter return new HtmlTagHydration(claimed_nodes, is_svg); } @@ -628,11 +667,11 @@ export function set_data_maybe_contenteditable(text: Text, data: unknown, attr_v } } -export function set_input_value(input, value) { +export function set_input_value(input: HTMLInputElement, value: string | null) { input.value = value == null ? '' : value; } -export function set_input_type(input, type) { +export function set_input_type(input: HTMLInputElement, type: string) { try { input.type = type; } catch (e) { @@ -640,7 +679,7 @@ export function set_input_type(input, type) { } } -export function set_style(node, key, value, important) { +export function set_style(node: HTMLElement, key: string, value: string, important: boolean) { if (value === null) { node.style.removeProperty(key); } else { @@ -648,7 +687,7 @@ export function set_style(node, key, value, important) { } } -export function select_option(select, value, mounting) { +export function select_option(select: HTMLSelectElementEx, value: string, mounting: boolean) { for (let i = 0; i < select.options.length; i += 1) { const option = select.options[i]; @@ -663,20 +702,20 @@ export function select_option(select, value, mounting) { } } -export function select_options(select, value) { +export function select_options(select: HTMLSelectElementEx, value: string) { for (let i = 0; i < select.options.length; i += 1) { const option = select.options[i]; - option.selected = ~value.indexOf(option.__value); + option.selected = ~value.indexOf(option.__value) as unknown as TODO; } } -export function select_value(select) { +export function select_value(select: HTMLSelectElementEx) { const selected_option = select.querySelector(':checked'); - return selected_option && selected_option.__value; + return selected_option && (selected_option as any).__value; } -export function select_multiple_value(select) { - return [].map.call(select.querySelectorAll(':checked'), option => option.__value); +export function select_multiple_value(select: HTMLSelectElement) { + return [].map.call(select.querySelectorAll(':checked'), (option: HTMLOptionElementEx) => option.__value); } // unfortunately this can't be a constant as that wouldn't be tree-shakeable @@ -720,9 +759,9 @@ export function add_iframe_resize_listener(node: HTMLElement, fn: () => void) { if (crossorigin) { iframe.src = "data:text/html,"; - unsubscribe = listen(window, 'message', (event: MessageEvent) => { + unsubscribe = listen(window, 'message', ((event: MessageEvent) => { if (event.source === iframe.contentWindow) fn(); - }); + }) as TODO); } else { iframe.src = 'about:blank'; iframe.onload = () => { @@ -752,13 +791,13 @@ export const resize_observer_border_box = new ResizeObserverSingleton({ box: 'bo export const resize_observer_device_pixel_content_box = new ResizeObserverSingleton({ box: 'device-pixel-content-box' }); export { ResizeObserverSingleton }; -export function toggle_class(element, name, toggle) { +export function toggle_class(element: HTMLElement, name: string, toggle: true) { element.classList[toggle ? 'add' : 'remove'](name); } export function custom_event(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent { const e: CustomEvent = document.createEvent('CustomEvent'); - e.initCustomEvent(type, bubbles, cancelable, detail); + e.initCustomEvent(type, bubbles, cancelable, detail as TODO); return e; } @@ -828,7 +867,7 @@ export class HtmlTag { this.n = Array.from(this.e.nodeName === 'TEMPLATE' ? (this.e as HTMLTemplateElement).content.childNodes : this.e.childNodes); } - i(anchor) { + i(anchor: Node) { for (let i = 0; i < this.n.length; i += 1) { insert(this.t, this.n[i], anchor); } @@ -849,8 +888,8 @@ export class HtmlTagHydration extends HtmlTag { // hydration claimed nodes l: ChildNode[] | void; - constructor(claimed_nodes?: ChildNode[], is_svg: boolean = false) { - super(is_svg); + constructor(claimed_nodes?: ChildNode[]) { + super(); this.e = this.n = null; this.l = claimed_nodes; } @@ -861,7 +900,7 @@ export class HtmlTagHydration extends HtmlTag { super.c(html); } } - i(anchor) { + i(anchor: NodeEx) { for (let i = 0; i < this.n.length; i += 1) { insert_hydration(this.t, this.n[i], anchor); } @@ -869,21 +908,21 @@ export class HtmlTagHydration extends HtmlTag { } export function attribute_to_object(attributes: NamedNodeMap) { - const result = {}; - for (const attribute of attributes) { + const result: Record = {}; + for (const attribute of attributes as unknown as Iterable) { result[attribute.name] = attribute.value; } return result; } export function get_custom_elements_slots(element: HTMLElement) { - const result = {}; - element.childNodes.forEach((node: Element) => { - result[node.slot || 'default'] = true; + const result: Record = {}; + element.childNodes.forEach((node) => { + result[(node as TODO).slot || 'default'] = true; }); return result; } -export function construct_svelte_component(component, props) { +export function construct_svelte_component(component: TODO, props: Record) { return new component(props); } diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 192f40140e4c..3996f1fe00eb 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -1,8 +1,10 @@ -import { Readable } from 'svelte/store'; +import { Readable, Subscriber, Writable } from 'svelte/store'; +import { SvelteComponent } from '../index.js'; +import { T$$ } from './types.js'; export function noop() {} -export const identity = x => x; +export const identity = (x: T): T => x; export function assign(tar: T, src: S): T & S { // @ts-ignore @@ -16,18 +18,18 @@ export function is_promise(value: any): value is PromiseLike { return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function'; } -export function add_location(element, file, line, column, char) { - element.__svelte_meta = { +export function add_location(element: Element, file: string | undefined, line: number, column: number, char: number) { + (element as Element & { __svelte_meta: any }).__svelte_meta = { loc: { file, line, column, char } }; } -export function run(fn) { - return fn(); +export function blank_object(): {} { + return Object.create(null); } -export function blank_object() { - return Object.create(null); +export function run(fn: Function) { + return fn(); } export function run_all(fns: Function[]) { @@ -38,13 +40,9 @@ export function is_function(thing: any): thing is Function { return typeof thing === 'function'; } -export function safe_not_equal(a, b) { - return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); -} +let src_url_equal_anchor: HTMLAnchorElement; -let src_url_equal_anchor; - -export function src_url_equal(element_src, url) { +export function src_url_equal(element_src: string, url: string) { if (!src_url_equal_anchor) { src_url_equal_anchor = document.createElement('a'); } @@ -52,52 +50,56 @@ export function src_url_equal(element_src, url) { return element_src === src_url_equal_anchor.href; } -export function not_equal(a, b) { +export function safe_not_equal(a: unknown, b: unknown) { + return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} + +export function not_equal(a: unknown, b: unknown) { return a != a ? b == b : a !== b; } -export function is_empty(obj) { +export function is_empty(obj: Record) { return Object.keys(obj).length === 0; } -export function validate_store(store, name) { +export function validate_store>(store: S, name: string) { if (store != null && typeof store.subscribe !== 'function') { throw new Error(`'${name}' is not a store with a 'subscribe' method`); } } -export function subscribe(store, ...callbacks) { +export function subscribe>(store: S, ...callbacks: Array>) { if (store == null) { return noop; } - const unsub = store.subscribe(...callbacks); + const unsub: any = store.subscribe(...(callbacks as [any])); return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub; } -export function get_store_value(store: Readable): T { - let value; - subscribe(store, _ => value = _)(); +export function get_store_value>(store: S): T { + let value: T; + subscribe(store, v => value = v)(); return value; } -export function component_subscribe(component, store, callback) { +export function component_subscribe>(component: SvelteComponent, store: S, callback: Subscriber) { component.$$.on_destroy.push(subscribe(store, callback)); } -export function create_slot(definition, ctx, $$scope, fn) { +export function create_slot(definition: any[], ctx: any[], $$scope: T$$, fn: Function) { if (definition) { const slot_ctx = get_slot_context(definition, ctx, $$scope, fn); return definition[0](slot_ctx); } } -function get_slot_context(definition, ctx, $$scope, fn) { +function get_slot_context(definition: any[], ctx: any[], $$scope: T$$, fn: Function) { return definition[1] && fn ? assign($$scope.ctx.slice(), definition[1](fn(ctx))) : $$scope.ctx; } -export function get_slot_changes(definition, $$scope, dirty, fn) { +export function get_slot_changes(definition: any[], $$scope: T$$, dirty: number[], fn: Function) { if (definition[2] && fn) { const lets = definition[2](fn(dirty)); @@ -115,25 +117,26 @@ export function get_slot_changes(definition, $$scope, dirty, fn) { return merged; } + // @ts-expect-error TODO return $$scope.dirty | lets; } return $$scope.dirty; } -export function update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn) { +export function update_slot_base(slot: any, slot_definition: any[], ctx: any[], $$scope: T$$, slot_changes: any, get_slot_context_fn: Function) { if (slot_changes) { const slot_context = get_slot_context(slot_definition, ctx, $$scope, get_slot_context_fn); slot.p(slot_context, slot_changes); } } -export function update_slot(slot, slot_definition, ctx, $$scope, dirty, get_slot_changes_fn, get_slot_context_fn) { +export function update_slot(slot: any, slot_definition: any[], ctx: any[], $$scope: T$$, dirty: number[], get_slot_changes_fn: Function, get_slot_context_fn: Function) { const slot_changes = get_slot_changes(slot_definition, $$scope, dirty, get_slot_changes_fn); update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn); } -export function get_all_dirty_from_scope($$scope) { +export function get_all_dirty_from_scope($$scope: T$$) { if ($$scope.ctx.length > 32) { const dirty = []; const length = $$scope.ctx.length / 32; @@ -145,48 +148,50 @@ export function get_all_dirty_from_scope($$scope) { return -1; } -export function exclude_internal_props(props) { - const result = {}; +export function exclude_internal_props

>(props: P) { + const result: Partial

= {}; for (const k in props) if (k[0] !== '$') result[k] = props[k]; return result; } -export function compute_rest_props(props, keys) { - const rest = {}; +export function compute_rest_props

>(props: P, keys: string[] | Set) { + const rest: Partial

= {}; keys = new Set(keys); for (const k in props) if (!keys.has(k) && k[0] !== '$') rest[k] = props[k]; return rest; } -export function compute_slots(slots) { - const result = {}; +export function compute_slots>(slots: S) { + const result = {} as Record; for (const key in slots) { result[key] = true; } return result; } -export function once(fn) { +export function once(fn: Function) { let ran = false; - return function(this: any, ...args) { + return function(this: any, ...args: unknown[]) { if (ran) return; ran = true; fn.call(this, ...args); }; } -export function null_to_empty(value) { - return value == null ? '' : value; +export function null_to_empty(value: T): T extends null | undefined ? '' : T { + return (value == null ? '' : value) as T extends null | undefined ? '' : T; } -export function set_store_value(store, ret, value) { +export function set_store_value>(store: S, ret: Node, value: T) { store.set(value); return ret; } -export const has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); +export function has_prop(obj: X, prop: Y): obj is X & Record { + return Object.prototype.hasOwnProperty.call(obj, prop); +} -export function action_destroyer(action_result) { +export function action_destroyer(action_result: { destroy?: () => void } | undefined) { return action_result && is_function(action_result.destroy) ? action_result.destroy : noop; } diff --git a/test/types/runtime/internal/lifecyle.test-types.ts b/test/types/runtime/internal/lifecyle.test-types.ts new file mode 100644 index 000000000000..6a3ebdd6666c --- /dev/null +++ b/test/types/runtime/internal/lifecyle.test-types.ts @@ -0,0 +1,35 @@ +// TODO: enable those tests when #7224 got merged + +// import { createEventDispatcher } from '$runtime/internal/lifecycle'; + +// const dispatch = createEventDispatcher<{ +// loaded: never +// change: string +// valid: boolean +// optional: number | null +// }>(); + +// // @ts-expect-error: dispatch invalid event +// dispatch('some-event'); + +// dispatch('loaded'); +// // @ts-expect-error: no detail accepted +// dispatch('loaded', 123); + +// // @ts-expect-error: detail not provided +// dispatch('change'); +// dispatch('change', 'string'); +// // @ts-expect-error: wrong type of detail +// dispatch('change', 123); +// // @ts-expect-error: wrong type of detail +// dispatch('change', undefined); + +// dispatch('valid', true); +// // @ts-expect-error: wrong type of detail +// dispatch('valid', 'string'); + +// dispatch('optional'); +// dispatch('optional', 123); +// dispatch('optional', null); +// // @ts-expect-error: wrong type of optional detail +// dispatch('optional', 'string'); diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 000000000000..5870e293c0c7 --- /dev/null +++ b/test/types/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "../../", + "paths": { + "$runtime/*": ["src/runtime/*"] + }, + // enable strictest options + "allowUnreachableCode": false, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "strict": true, + }, + "include": ["**/*.test-types.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index f2e055a2a95a..39532bfe48f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ // target node v8+ (https://node.green/) // the only missing feature is Array.prototype.values - "lib": ["es2017"], + "lib": ["es2017", "DOM"], "target": "es2017", "declaration": true,