Typescript: Ensure all properties use the same tuple type
Asked Answered
U

1

6

I've got a function with a generic that is a tuple of 1 or a tuple of 2 elements.
I want to ensure that all properties used in the function use the same length tuple.

type TypeA = [string] // Tuple of 1 element
type TypeB = [string, string] // Tuple of 2 elements
type Header = TypeA | TypeB

interface SomeObject<H extends Header> {
    prop1: H
    prop2: H
}

function useHeader<H extends Header>(someObject:SomeObject<H>) {
    // do something
}

useHeader({
    prop1: ["tuple of 1 element"],
    prop2: [
        "tuple of", 
        "2 elements"
    ] // <-- I want an error here, because prop1 and prop2 use diffrent tuples
})

I noticed that when I change TypeA to number and TypeB to string, then Typescript gives an error when I mix numbers and strings.
Is it possible to make Typescript generate an error when tuples of different length are used?

Undershorts answered 25/1, 2023 at 12:16 Comment(0)
D
7

Consider this approach:

type TypeA = [string] // Tuple of 1 element
type TypeB = [string, string] // Tuple of 2 elements

type Header = TypeA | TypeB

interface SomeObject<H extends Header> {
    prop1: H
    prop2: H & {} // <------- CHANGE IS HERE
}

function useHeader<H extends Header>(someObject: SomeObject<H>) {
    // do something
}

useHeader({
    prop1: ["tuple of 1 element"],
    prop2: [
        "tuple of",
        "2 elements"
    ] // <-- I want an error here, because prop1 and prop2 use diffrent tuples
})

Playground

This line H & {} means "make inference priority lower". In other words, TS will infer first H for prop1 and only then second prop2 and not simultaneously.

While this is not documented feature, Ryan Cavanaugh (development lead of the TypeScript team) says here that it's "...by design..." (w/emphasis) and "...probably going to work for the foreseeable future."

More undocumented features you can find in my blog. This particular hack was provided in this answer


WIhout undocumented tricks:

type TypeA = [string] // Tuple of 1 element
type TypeB = [string, string] // Tuple of 2 elements

type Header = TypeA | TypeB
type IsLengthEqual<T extends any[], U extends any[]> = U extends { length: T['length'] } ? U : never

interface SomeObject<H extends Header, H2 extends Header> {
    prop1: H
    prop2: IsLengthEqual<H, H2>
}

function useHeader<H extends Header, H2 extends Header>(someObject: SomeObject<H, H2>) {
    // do something
}

useHeader({
    prop1: ["tuple of 1 element"],
    prop2: [
        "tuple of",
        'dfg'
    ] // error
})

And with only one generic

type TypeA = [string] // Tuple of 1 element
type TypeB = [string, string] // Tuple of 2 elements

type Header = TypeA | TypeB

interface SomeObject<H extends Header> {
    prop1: H
    prop2: { [Prop in keyof H]: string }
}

function useHeader<H extends Header>(someObject: SomeObject<[...H]>) {
    // do something
}

useHeader({
    prop1: ["tuple of 1 element"],
    prop2: [
        "tuple of",
        'dfg'
    ] // error
})
Diarmuid answered 25/1, 2023 at 12:26 Comment(4)
that pretty cool, thanks! any idea why isn't it documented? is it experimental?Halfcaste
@HagaiKalinhoff not, it is not experimental, it is by design, see official commentDiarmuid
@T.J.Crowder good point. I have added another approach, also it looks like this behaviour is by design, so don't think it will be removed/disabled in the future. See commentDiarmuid
@captain-yossarianfromUkraine - Hey, it doesn't get better than that!Norm

© 2022 - 2024 — McMap. All rights reserved.