Skip to content

Commit bf00c4e

Browse files
Timo Lehnertzclaude
andcommitted
Resolve a default against the parameter list it is written in
A default is read where the parameters are, so its names resolve against the parameter list before the declarations around it. The first cut rendered a default as a type and stopped there, which left three ways for one to reach something other than the Rust it came from. All three are the same missing step, so `resolve_defaults` now makes it once, next to the `TsTypeParam` it settles, for the derive and `#[declare]` alike. **A type whose TypeScript name a parameter has taken.** The check only judged refs whose source is `TypeParam`, so a concrete type went through unexamined: struct Later<U = crate::T, T = String> // export interface Later<U = T, T = string> struct Earlier<T, U = crate::T> // export interface Earlier<T, U = T> `crate::T` loses its qualification when it renders. `Later` is a `TS2744` -- a default may only name a parameter declared before it -- and `Earlier` is worse, because it compiles and `T` now means the parameter. Neither can be spelled around inside a parameter list, where a parameter shadows, so the default goes and the trailing-run rule takes the defaults before it. **A default inside a namespace.** The alias collection and `prefix_type_refs` walked `type_ann` and nothing else, so a default was left to resolve against the namespace it is printed in: enum Outcome<T = crate::Error> { Done(T), Error(String) } `Outcome.Done`'s `T` meant the sibling variant rather than the interface -- nothing in the declaration is a syntax error, so it surfaced only where the two met. A default is collected and rewritten like any other reference now, and reaches the same `__OutcomeError` alias. **A renamed declaration named in a default** is not fixed here, and is not particular to defaults: a field of the same type emits the same dangling `Original`, which is #103. Both are pinned side by side so the day #103 lands, they move together. `#[tsify(type_params = "T = Renamed")]` says it in the meantime. The e2e crate follows #111's shape -- one source built under both features, since a default renders like any other type and `Option` and `HashMap` inside one move with the feature. It also records what a default does to #76: the signature still loses its argument, and `Wrapper` now resolves through the default rather than failing, so that line should change when #76 is fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 6222743 commit bf00c4e

11 files changed

Lines changed: 520 additions & 92 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Unreleased
44

5-
- **A default type parameter is now declared with its default.** `struct Foo<T = bool>(T)` declared `export type Foo<T> = T;` and now declares `export type Foo<T = boolean> = T;`, the same for interfaces, enums and `#[declare]` aliases. **Your `.d.ts` changes if you use one.** A default that cannot be honoured is dropped: one naming a parameter no field mentions, and then every default before it, since TypeScript only allows them on a trailing run
5+
- **A default type parameter is now declared with its default.** `struct Foo<T = bool>(T)` declared `export type Foo<T> = T;` and now declares `export type Foo<T = boolean> = T;`, the same for interfaces, enums and `#[declare]` aliases. **Your `.d.ts` changes if you use one.**
6+
- A default is read where the parameters are, so a name in it resolves against the parameter list before the declarations around it. One that would land somewhere other than the Rust says is dropped — naming a parameter that is declared nowhere or declared after it, or a type whose name a parameter has taken — and so is every default before it, since TypeScript only allows them on a trailing run. Inside `#[tsify(namespace)]`, a default is rewritten to the hoisted alias like any other reference, so a sibling variant cannot capture it
67
- While [#76](https://github.com/madonoharu/tsify/issues/76) is open this makes it quieter — `fn bar(foo: Ts<Foo<i64>>)` still writes `bar(foo: Foo)`, which the default now resolves to `Foo<boolean>` rather than raising `TS2314`
78

89
## v0.5.8
Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
/* tslint:disable */
22
/* eslint-disable */
3+
/**
4+
* A default is rendered like any other type, so the feature moves it.
5+
*/
6+
export interface Rendered<T = number | null, U = Record<string, number>> {
7+
t: T;
8+
u: U;
9+
}
10+
311
/**
412
* A reference names its argument, whatever the declaration defaults to.
513
*/
@@ -8,16 +16,56 @@ export interface Holder {
816
outcome: Outcome<number>;
917
}
1018

11-
declare namespace Outcome {
12-
export type Done<T = string> = { Done: T };
13-
export type Failed = { Failed: number };
19+
/**
20+
* A type of our own whose TypeScript name the parameter lists below also use.
21+
*/
22+
export interface T {
23+
z: number;
1424
}
1525

16-
export type Outcome<T = string> = Outcome.Done<T> | Outcome.Failed;
26+
/**
27+
* Accepted by TypeScript, but `T` there means the parameter and not the
28+
* interface — the same default reading as a different type.
29+
*/
30+
export interface Earlier<T, U> {
31+
t: T;
32+
u: U;
33+
}
34+
35+
/**
36+
* TypeScript reads the `T` in `U`'s default as the parameter declared after
37+
* it, and rejects that outright as `TS2744`.
38+
*/
39+
export interface Later<U, T = string> {
40+
u: U;
41+
t: T;
42+
}
43+
44+
export interface Error {
45+
m: string;
46+
}
1747

1848
export interface Wrapper<T = number> {
1949
value: T;
2050
}
2151

52+
type __OutcomeError = Error;
53+
/**
54+
* Read from inside the namespace, `Error` would reach the sibling variant. The
55+
* alias the namespace hoists its other references to is what it resolves to.
56+
*/
57+
declare namespace Outcome {
58+
export type Done<T = __OutcomeError> = { Done: T };
59+
export type Error = { Error: string };
60+
}
61+
62+
/**
63+
* Read from inside the namespace, `Error` would reach the sibling variant. The
64+
* alias the namespace hoists its other references to is what it resolves to.
65+
*/
66+
export type Outcome<T = Error> = Outcome.Done<T> | Outcome.Error;
67+
2268

2369
export function into_js(): Holder;
70+
71+
export function wrapped(v: Wrapper): void;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
/**
4+
* A default is rendered like any other type, so the feature moves it.
5+
*/
6+
export interface Rendered<T = number | undefined, U = Map<string, number>> {
7+
t: T;
8+
u: U;
9+
}
10+
11+
/**
12+
* A reference names its argument, whatever the declaration defaults to.
13+
*/
14+
export interface Holder {
15+
wrapped: Wrapper<number>;
16+
outcome: Outcome<number>;
17+
}
18+
19+
/**
20+
* A type of our own whose TypeScript name the parameter lists below also use.
21+
*/
22+
export interface T {
23+
z: number;
24+
}
25+
26+
/**
27+
* Accepted by TypeScript, but `T` there means the parameter and not the
28+
* interface — the same default reading as a different type.
29+
*/
30+
export interface Earlier<T, U> {
31+
t: T;
32+
u: U;
33+
}
34+
35+
/**
36+
* TypeScript reads the `T` in `U`'s default as the parameter declared after
37+
* it, and rejects that outright as `TS2744`.
38+
*/
39+
export interface Later<U, T = string> {
40+
u: U;
41+
t: T;
42+
}
43+
44+
export interface Error {
45+
m: string;
46+
}
47+
48+
export interface Wrapper<T = number> {
49+
value: T;
50+
}
51+
52+
type __OutcomeError = Error;
53+
/**
54+
* Read from inside the namespace, `Error` would reach the sibling variant. The
55+
* alias the namespace hoists its other references to is what it resolves to.
56+
*/
57+
declare namespace Outcome {
58+
export type Done<T = __OutcomeError> = { Done: T };
59+
export type Error = { Error: string };
60+
}
61+
62+
/**
63+
* Read from inside the namespace, `Error` would reach the sibling variant. The
64+
* alias the namespace hoists its other references to is what it resolves to.
65+
*/
66+
export type Outcome<T = Error> = Outcome.Done<T> | Outcome.Error;
67+
68+
69+
export function into_js(): Holder;
70+
71+
export function wrapped(v: Wrapper): void;

tests-e2e/test_defaults1/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ serde_json = "1.0"
1313
[dev-dependencies]
1414
wasm-bindgen-test = "0.3"
1515

16+
# `test_defaults_js1` shares this crate's `entry_point.rs` and turns this
17+
# on. Declared here as well so the `cfg` in the shared source is known to both.
18+
[features]
19+
js = ["tsify/js"]
20+
1621
[lib]
1722
path = "entry_point.rs"
1823
crate-type = ["cdylib"]

tests-e2e/test_defaults1/entry_point.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
//! A default type parameter is declared with its default.
2-
#![allow(deprecated)]
2+
//!
3+
//! This file is built twice: here with the default features, and by
4+
//! `test_defaults_js1`, which points its `[lib] path` at it and turns on `js`.
5+
//! The pair of reference outputs is what that feature changes — a default is
6+
//! rendered like any other type, so `Option` and `HashMap` inside one move with
7+
//! it.
8+
//!
9+
//! A default is read where the parameters are, so a name in it resolves against
10+
//! the parameter list before the declarations around it. The cases below are
11+
//! the ones where those two disagree.
12+
#![allow(deprecated, dead_code)]
13+
14+
use std::collections::HashMap;
315

416
use serde::{Deserialize, Serialize};
17+
use tsify::Ts;
518
use tsify::Tsify;
619
use wasm_bindgen::prelude::*;
720

@@ -10,11 +23,47 @@ pub struct Wrapper<T = i32> {
1023
value: T,
1124
}
1225

26+
/// A default is rendered like any other type, so the feature moves it.
27+
#[derive(Tsify, Serialize, Deserialize)]
28+
pub struct Rendered<T = Option<u32>, U = HashMap<String, u32>> {
29+
t: T,
30+
u: U,
31+
}
32+
33+
/// A type of our own whose TypeScript name the parameter lists below also use.
34+
#[derive(Tsify, Serialize, Deserialize)]
35+
pub struct T {
36+
z: u32,
37+
}
38+
39+
/// TypeScript reads the `T` in `U`'s default as the parameter declared after
40+
/// it, and rejects that outright as `TS2744`.
41+
#[derive(Tsify, Serialize, Deserialize)]
42+
pub struct Later<U = crate::T, T = String> {
43+
u: U,
44+
t: T,
45+
}
46+
47+
/// Accepted by TypeScript, but `T` there means the parameter and not the
48+
/// interface — the same default reading as a different type.
49+
#[derive(Tsify, Serialize, Deserialize)]
50+
pub struct Earlier<T, U = crate::T> {
51+
t: T,
52+
u: U,
53+
}
54+
55+
#[derive(Tsify, Serialize, Deserialize)]
56+
pub struct Error {
57+
m: String,
58+
}
59+
60+
/// Read from inside the namespace, `Error` would reach the sibling variant. The
61+
/// alias the namespace hoists its other references to is what it resolves to.
1362
#[derive(Tsify, Serialize, Deserialize)]
1463
#[tsify(namespace)]
15-
pub enum Outcome<T = String> {
64+
pub enum Outcome<T = crate::Error> {
1665
Done(T),
17-
Failed(u32),
66+
Error(String),
1867
}
1968

2069
/// A reference names its argument, whatever the declaration defaults to.
@@ -29,6 +78,18 @@ pub struct Holder {
2978
pub fn into_js() -> Holder {
3079
Holder {
3180
wrapped: Wrapper { value: 0 },
32-
outcome: Outcome::Failed(0),
81+
outcome: Outcome::Done(0),
3382
}
3483
}
84+
85+
// This records #76 the way the `test_generic_args1` references do, and what a
86+
// default does to it: the signature loses its argument, and `Wrapper` then
87+
// resolves through the default to `Wrapper<number>` rather than failing. The
88+
// Rust is `Wrapper<u32>`, so the two agree here only by accident of `i32` and
89+
// `u32` sharing a TypeScript name. This line should name its argument when #76
90+
// is fixed.
91+
#[wasm_bindgen]
92+
pub fn wrapped(v: Ts<Wrapper<u32>>) -> Result<(), JsError> {
93+
let _: Wrapper<u32> = v.to_rust()?;
94+
Ok(())
95+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "test_defaults_js1"
3+
publish = false
4+
version = "0.1.0"
5+
edition = "2021"
6+
7+
[dependencies]
8+
wasm-bindgen = "0.2"
9+
tsify = { path = "../..", version = "*" }
10+
serde = { version = "1.0", features = ["derive"] }
11+
serde_json = "1.0"
12+
13+
[dev-dependencies]
14+
wasm-bindgen-test = "0.3"
15+
16+
# The same source as `test_defaults1`, built with `js` instead. The pair of
17+
# reference outputs is what that feature changes.
18+
[features]
19+
default = ["js"]
20+
js = ["tsify/js"]
21+
22+
[lib]
23+
path = "../test_defaults1/entry_point.rs"
24+
crate-type = ["cdylib"]
25+
26+
[build-dependencies]
27+
wasm-bindgen-cli = "0.2"

0 commit comments

Comments
 (0)