Kan inte få TypeScript-genericskedjan att fungera
Jag bygger en funktion som accepterar en kedja av funktioner, där varje funktion omvandlar resultaten från den föregående. I det givna exemplet bör _context vara lika med { hello: 'world', something: 'else' }:
function withWorld<Context extends {}>(context: Context) {
return { ...context, hello: 'world' }
}
function withSomething<Context extends {}>(context: Context) {
return { ...context, something: 'else' }
}
test([withWorld, withSomething], (_context) => {})
Men när jag försöker beskriva detta beteende med generiska typer stöter jag på ett fel:
No overload matches this call:
Types of parameters 'contextSomething' and 'contextB' are incompatible.
Type 'unknown' is not assignable to type '{}'.
Här är koden (TypeScript-lekplats):
export interface TestFunction {
<A>(
conditions: [(contextSolo: {}) => A],
body: (contextBodySolo: A) => any
): any
<A, B>(
conditions: [(contextA: {}) => A, (contextB: A) => B],
body: (contextBody: B) => void
): any
}
const test: TestFunction = () => void 0
function withWorld<Context extends {}>(contextWithWorld: Context) {
return { ...context, hello: 'world' }
}
function withSomething<Context extends {}>(contextSomething: Context) {
return { ...context, something: 'else' }
}
test([withSomething], (_context) => {})
test([withWorld], (_context) => {})
test([withWorld, withSomething], (_context) => {})
När det finns en enda funktion i arrayen, härleder TypeScript typerna korrekt, även när exemplet är mer komplicerat och har ett initialt tillstånd (TypeScript playground).
Fullständig fel:
No overload matches this call.
Overload 1 of 2, '(conditions: [(contextSolo: {}) => any], body: (contextBodySolo: any) => any): any', gave the following error.
Argument of type '[<Context extends {}>(contextWithWorld: Context) => any, <Context extends {}>(contextSomething: Context) => any]' is not assignable to parameter of type '[(contextSolo: {}) => any]'.
Source has 2 element(s) but target allows only 1.
Overload 2 of 2, '(conditions: [(contextA: {}) => unknown, (contextB: unknown) => any], body: (contextBody: any) => void): any', gave the following error.
Type '<Context extends {}>(contextSomething: Context) => any' is not assignable to type '(contextB: unknown) => any'.
Types of parameters 'contextSomething' and 'contextB' are incompatible.
Type 'unknown' is not assignable to type '{}'.(2769)
Svar #1
Här har du ett fungerande exempel:
export interface TestFunction<Ctx> {
<A>(
conditions: [(context: Ctx) => A],
body: (context: A) => any
): any
<A, B>(
conditions: [(context: Ctx) => A & Ctx, (context: A & Ctx) => B],
body: (context: B & Ctx) => void
): any
}
const test: TestFunction<{ hello: 'world' }> = () => void 0
function withWorld<Context extends { hello: 'world' }>(context: Context) {
return { ...context, world: 'hello' }
}
function withSomething<Context extends {}>(context: Context) {
return { ...context, something: 'else' }
}
test([withSomething], (_context) => void 0)
test([withWorld], (_context) => void 0)
test([withWorld, withSomething], (_context) => void 0)
Jag lade uttryckligen till Ctx som snitt till varje argument
UPPDATERING
Här har du en generell lösning:
type Fn = (...args: any[]) => any
type MapObject<T extends Fn> = ReturnType<T>
type Elem = Fn
type Mapper<
Arr extends ReadonlyArray<Elem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends Elem
? Result & MapObject<H>
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<Elem>
? H extends Elem
? Mapper<Tail, Result & MapObject<H>>
: never
: never
: never
type Foo = { foo: 'foo' }
type Bar = { bar: 'bar' }
type Baz = { baz: 'baz' }
type Result = Mapper<[(arg: number) => Foo, (arg: Foo) => Bar, (arg: Bar) => Baz]> // Foo & Bar & Baz
export interface TestFunction<Ctx> {
<A>(
conditions: [(context: Ctx) => A],
body: (context: A) => any
): any
<A, B, C extends ReadonlyArray<any>>(
conditions: C,
body: (context: Mapper<C, Ctx>) => void
): any
}
const test: TestFunction<{ hello: 'world' }> = () => void 0
function withWorld<Context extends { hello: 'world' }>(context: Context) {
return { ...context, world: 'hello' }
}
function withSomething<Context extends {}>(context: Context) {
return { ...context, something: 'else' }
}
function withSomethingElse<Context extends {}>(context: Context) {
return { ...context, somethingElse: 'smth else' }
}
test([withSomething], (_context) => void 0)
test([withWorld], (_context) => void 0)
test([withWorld, withSomething, withSomethingElse] as const, (_context) => void 0)
Här kan du hitta andra exempel som kan vara intressanta för dig
Mutabla och immutabla arrayer
const mutable1 = [1, 2] // number[]
const mutable2 = [{ age: 1 }, { name: 'John' }]
type MutableLength = (typeof mutable2)['length'] // number, we don't know the length
// const mutable2: ({
// age: number;
// name?: undefined;
// } | {
// name: string;
// age?: undefined;
// })[]
// As you see, if you want to operate on mutable array, TS will just make a union type from all array customElements
const immutable = [{ age: 1 }, { name: 'John' }] as const
type ImmutableLength = (typeof immutable)['length'] // length is 2, we know exactly the length of array and the type of all elements
// Here, TS is able to say that your array has exact two elements
UPPDATERING, jag hoppas den sista :D
Mitt fel, jag trodde att det var omöjligt att göra det med föränderliga arrayer, men jag borde bara ha tittat på problemet från en annan vinkel.
Här är en fungerande lösning:
type Fn = (...args: any[]) => any
// credits goes https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
export interface TestFunction<Ctx> {
<A>(
conditions: [(context: Ctx) => A],
body: (context: A) => any
): any
<C extends Array<Fn>>(
conditions: C,
body: (context: UnionToIntersection<ReturnType<C[number]>>) => void
): any
}
const test: TestFunction<{ hello: 'world' }> = () => void 0
function withWorld<Context extends { hello: 'world' }>(context: Context) {
return { ...context, world: 'hello' }
}
function withSomething<Context extends {}>(context: Context) {
return { ...context, something: 'else' }
}
function withSomethingElse<Context extends {}>(context: Context) {
return { ...context, somethingElse: 'smth else' }
}
test([withSomething], (_context) => void 0)
test([withWorld], (_context) => void 0)
test([withWorld, withSomething, withSomethingElse], (_context) => void 0)
Svar #2
En tuppel är en array med individuella typer per index. Därför är längden fast.
En array har som standard en typ, samma för alla element, och längden är inte fast.
Tuples måste tvingas manuellt. Det är svårt att se eftersom ditt exempel är komplext. Att göra det fick felen att försvinna i Typescript Playground.
type Tuple<A,B>=[(contextA: {}) => A, (contextB: A) => B]
export interface TestFunction {
<A>(
conditions: [(contextSolo: {}) => A],
body: (contextBodySolo: A) => any
): any
<A, B>(
conditions: Tuple<A,B>,
body: (contextBody: B) => void
): any
}
const test: TestFunction = () => void 0
function withWorld<Context extends {}>(context: Context) {
return { ...context, hello: 'world' }
}
function withSomething<Context extends {}>(context: Context) {
return { ...context, something: 'else' }
}
test([withSomething], (_context) => {})
test([withWorld], (_context) => {})
test([withWorld, withSomething] as Tuple<typeof withWorld, typeof withSomething>, (_context) => {})
Den manuella tvångsåtgärden är i slutet där
[withWorld, withSomething]
ändrades till
[withWorld, withSomething] as Tuple<typeof withWorld, typeof withSomething>
Att manuellt tvinga tuples är vanligt i TS eftersom standarden är att skapa en array med det logiska ELLER av alla element.
Att skriva är som en typ:
type Default=(withWorld|withSomething)[]
Skriv detta i Typescript-lekplatsen
let a=[1,'1']
Placera din markör över 'a' så visas det
let a:(string|number)[]
och INTE
let a:[string,number]
