typescript - Funktionsparametertyp kollapsar till any när man använder rekursiv villkorlig typ


Detta är en uppföljning av ett problem som publicerats i Typescript-projektet på Github.

Joe Calzaretta (@jcalz) gav mig vänligt en lösning på mitt första problem med rekursiv villkorlig typ, men jag står nu inför ett annat problem där en funktionsparameter blir av typen any istället för den förväntade typen.

Här är ett utdrag av koden som visar problemet:

type ValidationContext<ValueType> = { value: ValueType }
type Constraint<ValueType, ConstraintType> =
    ConstraintType | ((context: ValidationContext<ValueType>) => ConstraintType)

type StringType = string | null | undefined
type StringConstraints = {
    required?: Constraint<StringType, boolean>,
    length?: Constraint<StringType, number>
}
type StringSchema = readonly ["string", StringConstraints]

type ObjectType = object | null | undefined
type ObjectConstraints = { required?: Constraint<ObjectType, boolean> } | undefined
type ObjectSchema<T extends ObjectType> =
    readonly ["object", ObjectConstraints, { [P in keyof T]: SchemaFor<T[P]> }]


type SchemaFor<T> =
    [T] extends [StringType] ? StringSchema :
    [T] extends [ObjectType] ? ObjectSchema<T> :
    never

type AnySchema = StringSchema | ObjectSchema<ObjectType>

type ValidateSchema<T> =
    T extends StringSchema ? StringSchema :
    T extends readonly ["object", ObjectConstraints, infer S extends ObjectType] ?
        readonly ["object", ObjectConstraints, { [K in keyof S]: ValidateSchema<S[K]> }] :
    never

function validate<const T extends AnySchema>(schema: T & ValidateSchema<T>, value: any) {
    return []
}

// Ok: context: ValidationContext<StringType>
validate(["string", { length: (context) => 1 }], null)

// Error: Parameter 'context' implicitly has an 'any' type
validate(["string", { required: (context) => true }], null)

// Error: Parameter 'context' implicitly has an 'any' type
validate(["object", { required: (context) => true }, {}], null)

// Ok:
//     required: true | (true & ((context: ValidationContext<ObjectType>) => boolean))
// and
//     required: true | (true & ((context: ValidationContext<StringType>) => boolean))
validate(["object", { required: true }, {
    name: ["string", { required: true }],
}], null)

Medan typen av context-parametern korrekt härleds i det första validate-anropet (dvs: ValidationContext<StringType> för length-begränsningen), blir den any så snart jag använder begränsningsnamnet required som delas av StringConstraints och ObjectConstraints.

Kan någon hjälpa mig att få de förväntade ValidationContext<...>-typerna?

Här är en TypeScript-playground-länk med koden ovan.


REDIGERA (förenklad version, länk till lekplats här):

type StringSchema = ["string", { required?: (value: string) => boolean }]
type NumberSchema = ["number", { required?: (value: number) => boolean }]

type AnySchema = StringSchema | NumberSchema

function validate(schema: AnySchema) {
    return schema[0]
}

// Error: Parameter 'value' implicitly has an 'any' type
validate(["string", { required: (value) => true }])

// Error: Parameter 'value' implicitly has an 'any' type
validate(["number", { required: (value) => true }])

Om jag använder schemadefinitioner med objekt (lekplats), försvann problemet:

type StringSchema = {
    kind: "string"
    constraints: { required?: (value: string) => boolean }
}
type NumberSchema = {
    kind: "number"
    constraints: { required?: (value: number) => boolean }
}

type AnySchema = StringSchema | NumberSchema

function validate(schema: AnySchema) {
    return schema.kind
}

// Ok: (parameter) value: string
validate({ kind: "string", constraints: { required: (value) => true } })

// Ok: (parameter) value: number
validate({ kind: "number", constraints: { required: (value) => true } })

Så vi kan vara ganska säkra på att det är relaterat till användningen av tupler.

Ska jag rapportera en bugg på Github?

Jag ska försöka att lämna ett PR om jag kan förstå hur man ställer in en anpassad version av tsc.

Svar #1

Detta är redan en känd TypeScript-bugg, rapporterad på microsoft/TypeScript#55632. TypeScript typar för närvarande inte callback-parametrar kontextuellt för diskriminerade unioner av tuplar i array-literaler. Faktum är att om du byter från arrayer till objekt får du den kontextuella typning du förväntar dig. För tillfället är lösningarna antingen att ge upp den kontextuella typningen (och bara annotera dina callback-parametrar) eller att ge upp tuplar (och använda objekt istället).

Denna bugg är märkt som "Help Wanted", så TS-teamet välkomnar pull-begäranden för att åtgärda den. Den intresserade läsaren som vill se detta åtgärdat istället för att kringgå det kan besluta sig för att skicka in en PR som åtgärdar det istället.

Svar #2

Som anges i avsnittet "REDIGERA" ovan är en lösning att ge upp tuples och istället använda objekttyper och litteraler.

Men det bästa alternativet skulle vara att åtgärda microsoft/TypeScript#55632-problemet: här är en PR microsoft/TypeScript#60434 som skulle ge en riktig lösning på detta problem om den slås ihop.