#Forcing a type to be treated as a tuple

24 messages · Page 1 of 1 (latest)

brisk bough
#

I have some "branded" tuple types in my code. They're just tuples, but I've added some properties so the type system will prevent them from getting them confused with eachother. (Ex: A GridPoint can't be passed into a function expecting a PixelPoint and vice-versa, even though they're both actually just [number, number] types).

But because this branding adds a property to the type, they're no longer tuple types as far as the checker is concerned. So I can't spread them anymore! If I do:

export type UnitPoint<Name extends string> = [number, number] & {
    __brand?: Name;
};

function foo(x: number, y: number): void {
    console.log(x, y);
}

const a: UnitPoint<"GridPoint"> = [1, 2];
foo(...a);

I get the error:
A spread argument must either have a tuple type or be passed to a rest parameter.
I can fix this at each callsite with foo(...a as [number, number]) but I'm wondering if there's a way to modify my type so that the checker knows that its actually just a tuple for this purpose.

jolly zincBOT
#
itsjohncs#0

Preview:```ts
export type UnitPoint<Name extends string> = [
number,
number
] & {
__brand?: Name
}

function foo(x: number, y: number): void {
console.log(x, y)
}

const a: UnitPoint<"GridPoint"> = [1, 2]
foo(...a)```

primal rampart
#

dang, i was really hoping this would work, but alas it does not:

export type UnitPoint<Name extends string> = [number, number] & {
  length: { __brand?: Name }
}
#

the check for "tuple-ness" is pretty persnickety

#

i guess you could slap the brand on one of the elements:

export type UnitPoint<Name extends string> = [number & { __brand?: Name }, number]
#

or all of 'em:

type BrandElements<T extends readonly unknown[], Name extends string> = {
    [K in keyof T]: T[K] & { __brand?: Name }
}
export type UnitPoint<Name extends string> = BrandElements<[number, number], Name>
#

i also might have hoped this would work:

declare const __brand: unique symbol;
export type UnitPoint<Name extends string> = [number, number] & {
    [__brand]?: Name;
};

since symbol keys are not enumerable there's nothing that could possibly go wrong (they'll never affect the behavior of a spread)

warm pike
#

Yup, the check for tuple type here is very specific. Changing it to accept this code seems pretty simple but I'll need to check for unwanted effects

jolly zincBOT
#
qhwood#0

Preview:```ts
interface I {
a: 1,
b: 2,
[__brand]: 3
}
const __brand: unique symbol = Symbol.for('__brand');

const i : I = { a: 1, b: 2, [__brand]: 3}

const notI = {...i};
// ^?

console.log(notI)

console.log({'notI.brand': notI[__brand]}) // { "notI.brand": 3 }
...```

primal rampart
#

wtf

jolly zincBOT
#
mkantor#0

Preview:```ts
const a = { a: 1, [Symbol('b')]: 3}
Object.defineProperty(a, 'c', { enumerable: false, value: 'wtf' }) // doesn't get spread!
Object.defineProperty(a, Symbol('d'), { enumerable: false, value: 'wtf' }) // doesn't get spread!

const b = { ...a }
console.log('symbol keys:', Object.getOwnPropertySymbols(b)) // => [Symbol(b)]
...```

primal rampart
#

i got lost in the spec trying to see how this behavior is documented, but i guess when MDN says:

Symbols are not enumerable in for...in iterations.
they really mean:
Whether their enumerable attribute is true or false, symbols aren't visible in for...in iterations.

#

TIL. i thought that it was just that symbol properties defaulted to enumerable: false

#

i'm not sure if/how this affects array spreads though. those use iteration semantics

#

yeah, technically the value could implement the iterable protocol and emit arbitrary values so you can never statically rely on the behavior of [...x], but if we make the same assumptions that the compiler already makes about array/argument spreads then it should be safe to ignore symbolic properties of tuples:

jolly zincBOT
#
mkantor#0

Preview:```ts
const a = ["a", "b", "c"]
a[Symbol() as any] = "something"

const b = [...a]
console.log(
"symbol keys:",
Object.getOwnPropertySymbols(b)
)
console.log("keys:", Object.keys(b))
console.log(
"own property names:",
Object.getOwnPropertyNames(b)
)```

primal rampart
#

@warm pike ☝️

warm pike
#

oh heck, this stuff is nutty.

#

this is all technically unsound, but probably okay most of the time, right?

primal rampart
#

yeah, e.g. here's an example of unsoundness in the current world:

jolly zincBOT
#
mkantor#0

Preview:```ts
const a: [number, string] = [1, "a"]
a[Symbol.iterator] = function* () {
yield "not a number"
}

const f = (a: number, b: string) => {
a.toExponential()
b.toUpperCase()
}

f(...a)```

primal rampart
#

but if anybody actually writes code like that, they deserve what's coming 😛

warm pike
#

Sure, but the average user doesn't deserve it when some daft lib dev does it