Investigated and reported by AI, reviewed by me.
Version
1.62.0 (regressed from 1.61.1)
Steps to reproduce
Passing a "branded"/nominal primitive — an intersection of a primitive and an object type, e.g. type IsoDate = string & { [__brand]: 'IsoDate' } — as the arg of page.evaluate() no longer type-checks in 1.62.0.
repro.ts:
import { test } from '@playwright/test'
declare const __brand: unique symbol
type Branded<T, B> = T & { [__brand]: B }
type IsoDate = Branded<string, 'IsoDate'>
declare function takesIsoDate(date: IsoDate): void
test('branded arg', async ({ page }) => {
const date = '2026-01-15' as IsoDate
// 1) explicitly annotated callback param
await page.evaluate((d: IsoDate) => void d, date)
// 2) inferred param, forwarded to something that wants the branded type
await page.evaluate(d => takesIsoDate(d), date)
// 3) branded value nested in an object arg
await page.evaluate(({ d }) => takesIsoDate(d), { d: date })
})
tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"module": "preserve",
"target": "es2022",
"moduleResolution": "bundler",
"skipLibCheck": true
},
"include": ["repro.ts"]
}
npm i --ignore-scripts @playwright/test@1.61.1 typescript@5.9.3 && npx tsc -p tsconfig.json # 0 errors
npm i --ignore-scripts @playwright/test@1.62.0 && npx tsc -p tsconfig.json # 3 errors
Expected behavior
tsc reports no errors, as with 1.61.1: Unboxed<IsoDate> should stay IsoDate, since a branded primitive is serializable and contains no handles.
Actual behavior
All three call sites fail. Unboxed<IsoDate> resolves to an object type whose String.prototype methods have been rewritten to return promises, and which is therefore no longer assignable to string:
repro.ts(13,23): error TS2769: No overload matches this call.
Overload 1 of 2, '(pageFunction: PageFunction<IsoDate, undefined>, arg: IsoDate, ...): Promise<undefined>', gave the following error.
Argument of type '(d: IsoDate) => undefined' is not assignable to parameter of type 'PageFunction<IsoDate, undefined>'.
Type '(d: IsoDate) => undefined' is not assignable to type '(arg: { readonly [x: number]: string; toString: () => Promise<string>; charAt: (pos: number) => Promise<string>; ... 46 more ...; [__brand]: "IsoDate"; }) => Promise<...> | undefined'.
Types of parameters 'd' and 'arg' are incompatible.
Type '{ readonly [x: number]: string; toString: () => Promise<string>; ... 46 more ...; [__brand]: "IsoDate"; }' is not assignable to type 'IsoDate'.
... is not assignable to type 'string'.
repro.ts(16,41): error TS2345: Argument of type '{ readonly [x: number]: string; toString: () => Promise<string>; ... }' is not assignable to parameter of type 'IsoDate'.
repro.ts(19,47): error TS2345: Argument of type '{ readonly [x: number]: string; toString: () => Promise<string>; ... }' is not assignable to parameter of type 'IsoDate'.
Additional context
The cause looks like the new callback support in evaluate (types/structs.d.ts).
1.61.1:
export type NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg);
export type Unboxed<Arg> =
Arg extends ElementHandle<infer T> ? T :
Arg extends JSHandle<infer T> ? T :
Arg extends NoHandles<Arg> ? Arg :
...
1.62.0 (added Arg extends Function ? never in NoHandles, and a function branch in Unboxed):
export type NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends Function ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg));
export type Unboxed<Arg> =
Arg extends ElementHandle<infer T> ? T :
Arg extends JSHandle<infer T> ? T :
Arg extends (...args: infer T) => infer R ? (...args: T) => CallbackResult<R> :
Arg extends NoHandles<Arg> ? Arg :
...
For string & { [__brand]: 'IsoDate' }, Arg extends object is true (the intersection contains an object type), so NoHandles maps over keyof Arg, which includes every String.prototype method. In 1.61 those methods took the Arg extends object branch and mapped to {}-like types, so Arg extends NoHandles<Arg> still held and Unboxed<Arg> returned Arg unchanged. In 1.62 the new Arg extends Function ? never branch maps each method to never, so Arg extends NoHandles<Arg> is now false, Unboxed falls through to the object-mapping branch, and the new function branch rewrites each method to (...args) => Promise<...>.
A guard for primitives before the object branches would fix it, e.g.:
export type Unboxed<Arg> =
Arg extends ElementHandle<infer T> ? T :
Arg extends JSHandle<infer T> ? T :
Arg extends string | number | boolean | bigint | symbol | null | undefined ? Arg : // <-- branded primitives
Arg extends (...args: infer T) => infer R ? (...args: T) => CallbackResult<R> :
...
Branded primitives are a common way to model nominal types (IsoDate, UnixTimestamp, UserId, …), so this affects any suite that passes such values into the page. Current workaround is to widen to the base primitive at the boundary and re-brand inside the callback, which loses type safety on the argument.
Environment
- Operating System: macOS 26.5.2
- Playwright Version: 1.62.0 (1.61.1 is fine)
- TypeScript Version: 5.9.3 (also reproduces with 6.0.3)
- Node.js Version: v24.18.0
- Browser: n/a (types only)
Investigated and reported by AI, reviewed by me.
Version
1.62.0 (regressed from 1.61.1)
Steps to reproduce
Passing a "branded"/nominal primitive — an intersection of a primitive and an object type, e.g.
type IsoDate = string & { [__brand]: 'IsoDate' }— as theargofpage.evaluate()no longer type-checks in 1.62.0.repro.ts:tsconfig.json:{ "compilerOptions": { "strict": true, "noEmit": true, "module": "preserve", "target": "es2022", "moduleResolution": "bundler", "skipLibCheck": true }, "include": ["repro.ts"] }Expected behavior
tscreports no errors, as with 1.61.1:Unboxed<IsoDate>should stayIsoDate, since a branded primitive is serializable and contains no handles.Actual behavior
All three call sites fail.
Unboxed<IsoDate>resolves to an object type whoseString.prototypemethods have been rewritten to return promises, and which is therefore no longer assignable tostring:Additional context
The cause looks like the new callback support in
evaluate(types/structs.d.ts).1.61.1:
1.62.0 (added
Arg extends Function ? neverinNoHandles, and a function branch inUnboxed):For
string & { [__brand]: 'IsoDate' },Arg extends objectis true (the intersection contains an object type), soNoHandlesmaps overkeyof Arg, which includes everyString.prototypemethod. In 1.61 those methods took theArg extends objectbranch and mapped to{}-like types, soArg extends NoHandles<Arg>still held andUnboxed<Arg>returnedArgunchanged. In 1.62 the newArg extends Function ? neverbranch maps each method tonever, soArg extends NoHandles<Arg>is now false,Unboxedfalls through to the object-mapping branch, and the new function branch rewrites each method to(...args) => Promise<...>.A guard for primitives before the object branches would fix it, e.g.:
Branded primitives are a common way to model nominal types (
IsoDate,UnixTimestamp,UserId, …), so this affects any suite that passes such values into the page. Current workaround is to widen to the base primitive at the boundary and re-brand inside the callback, which loses type safety on the argument.Environment