I was trying to convert my styled-component
mixin module to typescript but encountered a problem that I can't solve (in the chain
method). Live playground here.
// Sample mixin methods
const fullscreen = () => `
width: 100vw;
height: 100vh;
`
const squared = (size: string) => `
width: ${size};
height: ${size};
`
interface mixinInterface {
fullscreen: typeof fullscreen
squared: typeof squared
}
type chainedMixinInterface = {
[P in keyof mixinInterface]: (...args: Parameters<mixinInterface[P]>) => () => string
}
const mixins: mixinInterface & { chain: () => chainedMixinInterface } = {
fullscreen,
squared,
// ...other mixin methods
chain: function () {
const chainedObject: Partial<chainedMixinInterface> = {}
let accumulatedReturn = ''
// get mixin keys without 'chain'
const keys = Object.keys(mixins).filter(key => key !== 'chain') as (keyof mixinInterface)[]
keys.forEach(key => {
const mixinFunction = mixins[key]
// wraps subsequent mixin methods
chainedObject[key] = function (...args: Parameters<typeof mixinFunction>) {
accumulatedReturn += mixinFunction(...args) // Error: A spread argument must either have a tuple type or be passed to a rest parameter.
const returnAllChainedValues = () => accumulatedReturn
return Object.assign(returnAllChainedValues, this) // pass along the methods to be chained
}
})
return chainedObject as chainedMixinInterface
}
}
This is the use case for chain:
const Div = styled.div`
// returns a function that returns the chained results
// which will be called by styled-component's tag function
${mixins
.chain()
.fullscreen()
.squared('50px')
// ...other mixin methods
}
`
By calling the chain
function, it wraps subsequent calls to mixin methods. Instead of returning a string, it returns a function that returns the accumulated result (string) from all of the chained methods.
Even though I specified ...args
as the parameters of the mixinFunction
that is currenting being iterated on, it isn't a tuple, but a union of tuples. Therefore, TypeScript throws an error in the following line, where I spread the args
in the mixinFunction
call.
I haven't found a way to solve this problem. Is there a way to sidestep this issue by only modifying the typing, without changing the underlying JavaScript?
TypeScript does not do type narrowing on a case basis, it only narrows to general types. Consider the following code
interface MyType {
n: number,
s: string
}
function selfAssign(a: MyType, key: keyof MyType) {
a[key] = a[key]; // Error: Type 'string | number' is not assignable to type 'never'
}
selfAssign({ n: 3, s: 'abc'}, 'n');
Even though a[key] = a[key]
is clearly typesafe, TS doesn't have a type that represents "type of a[key]
whatever it is".
For all TS knows, you're trying to assign a value that might either be a number or a string, to a property that either only accepts numbers or only accepts strings.
It says the lefthand a[key]
is of type never
because no value can satisfy both number
and string
. In a slightly different case the error would be different but the princip is the same.
In your case, mixinFunction
can be one of a few functions with a different signature each, so the type of ...args: Parameters<typeof mixinFunction>
is the intersection of all those possibilities rather than a particular tuple, and therefore it's not possible to call mixinFunction
with ...args
as the parameters.
If you don't want to change the underlying JS code in a way that'll allow TS to recognize the type match, you can relax the type safety by performing a cast. One such option that seems to work is
accumulatedReturn += (mixinFunction as unknown as (...args: any) => () => string)(...args)
The above code casts mixinFunction
to (...args: any) => () => string
which allows calling it with any parameters, but it's still relatively safe because it only affects that specific line where you immediately call the function with the actual ...args
.
The intermediate cast to unknown
is apparently also needed because otherwise TS complains about the cast not being reasnoable.