microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.3k stars 12.39k forks source link

Proposal: strict and open-length tuple types #6229

Closed Igorbek closed 6 years ago

Igorbek commented 8 years ago

Update: converted to proposal.

Background

Currently, tuples are arrays that are restricted in minimum length, but not in maximum:

var t1: [number, number] = [1]; // this doesn't work
var t2: [number] = [1, 2]; // this works

This makes harder to predict types or errors in some scenarios:

var t1: [number, string];
var t2 = [...t1, ...t1]; // could be inferred to be [number, string, number, string], but must be inferred as [number, string] (now it's simply inferred as (number|string)[])

var t3: [number, string] = [1, "a"];
t3[2]; // ok, but must be an error

There also might be difficult to adopt variadic kinds, especially in type construction:

function f<...T>(...rest: [...T]): [...T, ...T] {
  var rest1: [...T] = [...rest, 1]; // it will be acceptable due to current rules
  return [...rest1, ...rest1]; // due to types it seems to be [...T, ...T], but actually is [...T, number, ...T, number]
}

Proposal

(1) Restrict tuple instance to match exact length

var t1: [number, string] = [1, "a"]; // ok
var t2: [number, string] = [1]; // error (existing)
var t3: [number, string] = [1, "a", "b"]; // error (new)

(2) Introduce open length tuple types

Open-length tuple types will be the same as tuple types are now.

var t1: [number, string, ...] = [1, "a", 2, "b"]; // same as current tuples are now
var t2: [number, string, ...(number|string|boolean)[]]; // explicitly type rest elements -- syntax option 1 -- consistent with rest parameters
var t3: [number, string, ...number|string]; // explicitly type rest elements -- syntax option 2

// strict tuple type can be implicitly converted to open length tuple type
var t4: [number, string, string];
var t5: [number, string, ...] = t4; // ok
var t6: [number, ...] = t4; // error, 'number|string' cannot be converted to 'number'
var t6: [number|string, ...] = t4; // ok
var t7: [number, ...(number|string)[]] = t4; // ok

(3) Improve contextual type inference for spread operators on tuples

var t1: [number, string];
var t2: [number, string, number, string] = [...t1, ...t1]; // it's proven now

var t3: [number, ...];
var t4: [string, ...];
var t4: [number, number|string, ...] = [...t3, ...t4]; // this also can be proven

Related issues

This addresses:

This is definitely a breaking change.

Igorbek commented 8 years ago

Any thoughts on this? Do you think it could be proposed?

JsonFreeman commented 8 years ago

You'd still have the problem of calling push or pop on the tuple. Or these subtler variants:

array[array.length] = 0;
array.length--;
zpdDG4gta8XKpMCd commented 8 years ago

just a personal observation, as of now the following way of doing tuples gets more predictable results than the official tuples that are mostly arrays

interface T2<a, b> {
    0: a;
    1: b;
}
function t2 <a, b>(one: a, two: b) : T2 <a, b> {
    return <any> [one, two];
}
Igorbek commented 8 years ago

@JsonFreeman hm, fair. However we have similar things that can cheat type system. Such as array variance.

var animals: Animal[] = dogs;
animals.push(cat);
dogs[dogs.length-1].woof(); // boom

BTW, array.length-- breaks existing rules too.

Option 1 - restrict such operations on fixed-length tuples. So let's say if array boundaries wasn't proven - give an error. Option 2 - allow to shoot the leg, as we do in other cases.

dead-claudia commented 8 years ago

By the way, :+1: for this proposal. It seems to me a necessity for solving the problem with variadic types. No matter how you try to solve the variadic problem (I've seen several ideas already), this comes up and gets in the way every single time.

Artazor commented 8 years ago

Ability to distinguish strict tuples and open tuples looks reasonable.

dead-claudia commented 8 years ago

@Igorbek My opinion on that:

Artazor commented 8 years ago

@isiahmeadows

Fixed-length tuples should only have a subset of the Array operations

Agree. Moreover, it seems to me that fixed-length tuples are needed only in read-only scenarios...

dead-claudia commented 8 years ago

@Artazor There are times when it's nice to be able to write to a tuple. It's not frequent, but it's occasionally helpful. I would be okay with copyWithin with limited semantics. The biggest reason I'm interested in fixed-length tuples is that it would help solve the variadic problem with bind tremendously (that combined with non-strict type checking).

Artazor commented 8 years ago

@isiahmeadows Imagine, that [T1,T2,T3] means strict tuple. Let's try to write a problematic code:

var a: [number, boolean] = [1, true] //strict
var b: [number, boolean, number, boolean] //strict
b = [...a, ...a] // ok
a[0] = 2; // ok (statically)
b = [...a, ...a] // still ok
a[a[0]] = 3; // can we prevent this at compile time? (doubt)
b = [...a, ...a] // oops!
dead-claudia commented 8 years ago

@Artazor

I feel it should be restricted to n-tuples of just a single type. As for indexed access, it should be unsafe, because otherwise it's much more complicated for the compiler, and if you're resorting to this over plain objects in most cases, either the code probably already smells of feces (little the language can do to help you here), or you know what you're doing, and need that indexed access for performance reasons (e.g. an 8-entry table, which the compiler will infer).

As for varying types, it should be read-only, but unsafe read access is pretty much the only way to do it in practice. Otherwise, it's unnecessary boilerplate outside of destructuring. Matter of fact, in many of these kinds of cases, Haskell prefers crashing over returning a Maybe, since it's far faster and chances are, you probably already have an idea whether your index is within range.

Remember, you can only do so much statically - some things are literally undetectable until runtime, no matter how powerful your type system is.

JsonFreeman commented 8 years ago

I agree with the general sentiment of wanting fixed length tuples. The reason I am worried about the length of the tuple not being perfectly enforceable is that if it is used to solve the variadic bind typing, you won't just get a tuple/array of the wrong length. You'll get a function with the wrong number of arguments! For some reason that seems a lot worse to me than a tuple of the wrong length, or even arguments of the wrong types.

dead-claudia commented 8 years ago

@JsonFreeman That's one of the main reasons I want fixed-length tuples. Using tuples for variadic types won't be a problem with fixed-length tuples. Plus, it's more type safe, which is always a plus. If you're okay with open-ended tuples that subtype Arrays, in which the length can change, it's probably better to be explicit about that.

(I'd rather opt out of type safety than in.)

JsonFreeman commented 8 years ago

I agree with that, my point is that you still have to be okay with the tuple length being wrong in a case like this.

dead-claudia commented 8 years ago

@JsonFreeman I did suggest removing the mutating methods from the fixed-length tuple interface.

Igorbek commented 8 years ago

@isiahmeadows I'd say would be better to remove length-mutating methods only, such as push, pop, and make length readonly. However, it would be still able to assign to a open-length tuple variable and mutate its length.

dead-claudia commented 8 years ago

Oh, and you might want to include splice as well. That can mutate length.

On Thu, Mar 10, 2016, 14:13 Isiah Meadows impinball@gmail.com wrote:

I meant that implicitly... Sorry about that.

On Thu, Mar 10, 2016, 14:12 Igor Oleinikov notifications@github.com wrote:

@isiahmeadows https://github.com/isiahmeadows I'd say would be better to remove length-mutating methods only, such as push, pop, and make length readonly. However, it would be still able to assign to a open-length tuple variable and mutate its length.

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/6229#issuecomment-195003956 .

dead-claudia commented 8 years ago

I meant that implicitly... Sorry about that.

On Thu, Mar 10, 2016, 14:12 Igor Oleinikov notifications@github.com wrote:

@isiahmeadows https://github.com/isiahmeadows I'd say would be better to remove length-mutating methods only, such as push, pop, and make length readonly. However, it would be still able to assign to a open-length tuple variable and mutate its length.

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/6229#issuecomment-195003956 .

Artazor commented 8 years ago

Interesting, that these problems are expressible in the following way:
If one value type B extends memory layout of the other value type A, then actually only the reference to the first type B* is the subtype of the reference to the second one A* (compare with inheritance in C++). As we know arrays in JavaScript are reference types (not a value types), at the same time fixed length tuples capable of being used in variadic equations resolution should be value types. That is why @JsonFreeman has intuition that tuples are subtype of arrays.

dead-claudia commented 8 years ago

@Artazor That seems about right AFAICT.

JsonFreeman commented 8 years ago

The way tuples are defined in TypeScript entails that they are subtypes of arrays. This is intuitive, and it works pretty well in most cases. But it definitely has its problems, and the variadic matching is indeed one of those problems.

dead-claudia commented 8 years ago

Now that I think about it, array literals should be castable + assignable to Array as well as all tuple types, and this will have to be doable on the language level. Otherwise, you have a huge back-compat problem.

// If either of these fail, that's a lot of existing code breakage.
let list1 = <number[]> [1, 2, 3]
let list2: number[] = [1, 2, 3]

Just a thought. That's all.

ghost commented 8 years ago

Can we also have optional tuple elements? E.g. [ number, number? ].

Similar to [ number ]|[ number, number ] except not an error to use in destructuring. E.g.

const foobar: [ number ]|[ number, number ] = [ 1 ],
    [ foo, bar = undefined ] = foobar; // currently an error

Similar to [ number, number|void ] except not an error to assign [ number ]. E.g.

const foobar: [ number, number|void ] = [ 1 ]; // currently an error
dead-claudia commented 8 years ago

@errorx666 [number, number?] is already a valid type now, and it carries similar semantics to [number, number|void].

Also, I'm not entirely convinced optional tuple elements are even a necessary feature.

ghost commented 8 years ago

@isiahmeadows: Neither of those types allow [ 1 ]. I ran into a use-case where I wanted an array of exactly one or exactly two numbers. The two solutions I tried (shown above) both resulted in compilation errors (despite working fine in the emit).

dead-claudia commented 8 years ago

I meant your suggestion conflicted with those, not that those already worked for your own use case.

On Tue, Aug 30, 2016, 09:57 error notifications@github.com wrote:

@isiahmeadows https://github.com/isiahmeadows: Neither of those types allow [ 1 ]. I ran into a use-case where I wanted an array of exactly one or exactly two numbers. The two solutions I tried (shown above) both resulted in compilation errors (despite working fine in the emit).

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/6229#issuecomment-243446947, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBJDCxum4tvSQACUPpamSCQmqRSaTks5qlDbjgaJpZM4G6w_5 .

Igorbek commented 8 years ago

@DanielRosenwasser does anything prevent from considering this proposal? Any thoughts on this from the team? Could it be taken for the next slog? I see only positive feedback from the community and feel it could really improve typesystem.

danielearwicker commented 8 years ago

Now there are number literal types the indexer for a tuple could be restricted appropriately:

const t: [number, string] = [100, "200"];

// type is minimally something like (but see #2049):
interface T {
    0: number;
    1: string;
    [index: 0 | 1]: number|string;
}

const i1: number = 0;
t[i1]; // error

const i2: 0 | 1 = 0;
t[i2]; // fine, of type number|string

Another breaking change, but I guess this whole proposal would need a --strictTuples flag anyway.

Related: https://github.com/Microsoft/TypeScript/issues/2049

eric-wieser commented 7 years ago

@Igorbek: Why is this example from your proposal an error?

var t4: [number, string, string];
var t5: [number, string, ...] = t4; // ok
var t6: [number, ...] = t4; // error, 'number|string' cannot be converted to 'number'
danielearwicker commented 7 years ago

@eric-wieser [number, ...] would mean "array of numbers of minimum length 1". It doesn't allow its elements to be strings.

Igorbek commented 7 years ago

@eric-wieser yes, @danielearwicker is right. I should've been clearer there. I'm proposing the syntax for open-length tuples in the form of rest arguments with a shortcut supported:

[T1, T2, ...T3[]] // means allows T1 for [0], T2 for [2] and T3 for [3], [4], and so on
[T1, T2, ...] // is equivalent to [T1, T2, ...(T1|T2)[]]
eric-wieser commented 7 years ago

I don't see why [T1, T2, ...] -> [T1, T2, ...(T1|T2)[]] is desirable at all. I'd argue the only sensible interpretation is [T1, T2, ...any[]], and if you want anything else you should be explicit.

The only argument I can see for this is that it matches the existing behaviour of [T1, T2], but since this is already a breaking change, now seems like a good time to question old design decisions.

danielearwicker commented 7 years ago

As a fan of --noImplicitAny, I definitely don't want new ways to implicitly allow any. Should always be explicitly requested IMO. I'd prefer TS to reject ... without an explicit type than silently assume any.

Igorbek commented 7 years ago

Totally agree. We definitely don't need another implicit any. As a starting point, we can delay introducing implicit ... and allow just explicit open-length tuple syntax [T1, T2, ...T3[]]

carlansley commented 7 years ago

Is there any syntactic reason there couldn't be single-type tuple definitions based on index ranges, e.g.:

const z: number[2] = [1,2];
const y: number[2...] = [1,2,3,4];
let x: string[2...3];
x = ['a']; // error
x = ['a','b']; // ok
x = ['a','b','c']; // ok
x = ['a','b','c','d']; // error
christyharagan commented 7 years ago

Hey guys,

Copying my comment over from #10727

In short, I was raising the idea of an "array-spread" type, which seems could be an extension to/part of this proposal. Seeing what you guys think...


Essentially, the use-case I'm looking to solve is the ability to type an Array/Tuple where the first n-elements are typed, and then the remaining elements are typed as an unbounded list. To be more precise (and correct my loose language), consider:

type AT_LEAST_ONE_ELEMENT = [string, ...string]

// To support the case where a CSV row is parsed, and the first 4 columns are known, but the remaining columns (if any) will be strings
type FLEXIBLE_CSV_ARRAY = [string, number, string, boolean, ...string]

For a few bonus points, it would be great if intersections worked like:

type ONE_ELEMENT = [string]
type MANY_ELEMENTS = string[]

type AT_LEAST_ONE_ELEMENT = ONE_ELEMENT & MANY_ELEMENTS // === [string ...string]
Igorbek commented 7 years ago

@christyharagan You're right, your idea seems to be a use case to my proposal. In fact, I believe it's already covered. The syntax you're proposing is exactly as I proposed in option 2:

var t3: [number, string, ...number|string]; // explicitly type rest elements -- syntax option 2

However, I think it's not consistent with other language constructs. It denotes a type of a single element, when in other cases where spread is applied it points to a container. So I'm in favor of only allowing the rest to be of array type:

type AT_LEAST_ONE_ELEMENT = [string, ...string[]]; // in fact it is equal to what [string] now

// To support the case where a CSV row is parsed, and the first 4 columns are known, but the remaining columns (if any) will be strings
type FLEXIBLE_CSV_ARRAY = [string, number, string, boolean, ...string[]]

Just to summarize, my proposal in its open-length tuples part, is to introduce an explicit types of rest elements in a tuple:

type A = [string, number, ...(string|number)[]]; // now it is typed with [string, number]
type B = [string, number, ...]; // implicit syntax, same as above, may not be good enough (as it is implicit)
type C = [string, number, ...boolean[]]; // no way to express it currently

I'm going to add more to the proposal of what type operations can be applied (spreads, unions, intersections), assignability, etc.

dead-claudia commented 7 years ago

@Igorbek Whatever ends up picked here for unions/intersections will also translate for variadic types, because this is in fact a subset of functionality for that.

(You can model a non-overloaded function's arguments as an open length tuple, and the spread array type is mostly identical to existing rest types, which is where I'm coming from.)

ondratra commented 7 years ago

I would love to see this proposal accepted too(actually I was a bit suprised this isn't a thing in a typescript already). Is there any reason why it haven't been accepted yet?

My problem is similar as in @Igorbek example with CSV. In my case with form validator configuration it should look like this

// proposed solution
interface ValidationConfigInterface {
    [key: string]: (string|[string, ...any[]])[];
}
const validations: ValidationConfigInterface = {
   email: ['required', 'isEmail'],
   password: ['required', ['minLength', 5]],
   age: ['required', 'isNumber', ['range', 13, 18, 'anotherExtraParameterOfAnyType']]
}

That's much more descriptive than currently available solution that might lead to confusion that multiple parameters are not accepted(age seems invalid).

// nowadays solution
interface ValidationConfigInterface {
    [key: string]: (string|[string, any])[];
}
KiaraGrouwstra commented 7 years ago

I tried t3, but failed:

var t3b: { 0: number, 1: string } = [1, "a"];
t3b[2]; // still no error, and can't overwrite the numerical index to anything stricter than `number | string` :(

Interestingly it does error if you try this member access on the type level instead. This means alternatives where the operation would be specified through a typing, such as _.get or R.prop, could serve as type-safer alternatives to regular member access. That seems more verbose, but with currying + function composition has its pros as well.

As to making this fail:

var t5: [number, string] = [1, "a", "b"]; // error (new)

It seems RHS tuples can take extra properties, objects can't. If I can fix TupleToObject, maybe this would do:

type AddPrototype<T> = Pick<T, keyof T>;
type ArrProto<T extends any[]> = AddPrototype<T> & {
    [Symbol.iterator]: () => IterableIterator<T[-1]>,
    [Symbol.unscopables]: () => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }
} & { [i: number]: T[-1] };

var t1: [number, string] = <ArrProto<[1, "a"]>>[1, "a"];
// ^ ok
// v we're good if one of these errors. a/b don't, and c/d/e still break from the `TupleToObject` bug...
var t5a: [number, string] = [1, "a", "b"];
var t5b: [number, string] = <ArrProto<[1, "a", "b"]>> [1, "a", "b"];
var t5c: TupleToObject<[number, string]> = [1, "a", "b"];
var t5d: TupleToObject<[number, string]> = <TupleToObject<[1, "a", "b"]>> [1, "a", "b"];
var t5e: TupleToObject<[number, string]> = <ArrProto<[1, "a", "b"]>> [1, "a", "b"];

Still explicit conversions, type dependencies, non-DRY on the expression level, still broken, and t3... isn't ideal either. :/

KiaraGrouwstra commented 7 years ago

well, I got t3 to error at least, though it ain't pretty:

export type Obj<T> = { [k: string]: T };
export type TupleHasIndex<Arr extends any[], I extends number> = ({[K in keyof Arr]: '1' } & Array<'0'>)[I];
// ^ #15768, TS2536 `X cannot be used to index Y` on generic
export type ObjectHasKey<O extends {}, K extends string> =
    ({[K in keyof O]: '1' } & Obj<'0'>)[K];
export type NumberToString = ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31','32','33','34','35','36','37','38','39','40','41','42','43','44','45','46','47','48','49','50','51','52','53','54','55','56','57','58','59','60','61','62','63','64','65','66','67','68','69','70','71','72','73','74','75','76','77','78','79','80','81','82','83','84','85','86','87','88','89','90','91','92','93','94','95','96','97','98','99','100','101','102','103','104','105','106','107','108','109','110','111','112','113','114','115','116','117','118','119','120','121','122','123','124','125','126','127','128','129','130','131','132','133','134','135','136','137','138','139','140','141','142','143','144','145','146','147','148','149','150','151','152','153','154','155','156','157','158','159','160','161','162','163','164','165','166','167','168','169','170','171','172','173','174','175','176','177','178','179','180','181','182','183','184','185','186','187','188','189','190','191','192','193','194','195','196','197','198','199','200','201','202','203','204','205','206','207','208','209','210','211','212','213','214','215','216','217','218','219','220','221','222','223','224','225','226','227','228','229','230','231','232','233','234','235','236','237','238','239','240','241','242','243','244','245','246','247','248','249','250','251','252','253','254','255'];
export type Inc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256];
export type Overwrite<K, T> = {[P in keyof T | keyof K]: { 1: T[P], 0: K[P] }[ObjectHasKey<T, P>]};
export type TupleToObject<R extends any[], I extends number = 0, Acc = {}> =
    { 1: TupleToObject<R, Inc[I], Overwrite<Acc, { [P in NumberToString[I]]: R[I] }>>, 0: Acc }[TupleHasIndex<R, I>];

const foo: [1, "a"] = [1, "a"]; // no cast with #16389
var t3: TupleToObject<typeof foo> = foo;
t3[2]; // error with `noImplicitAny`: Element implicitly has an 'any' type because type ... has no index signature.
KiaraGrouwstra commented 7 years ago

Well, Ramda typings also just ran into this issue, https://github.com/types/npm-ramda/issues/173#issuecomment-320515661. Specifically, after a function overload asking for a higher-length tuple failed, it fell through to an overload asking for a unary tuple, which then matched, going against desired behavior. Not seeing clear alternatives (based on overloads) that could do without this.

KiaraGrouwstra commented 7 years ago

Potential solution, tie tuples to a new Tuple interface (sub-typing ReadOnlyArray), following the suggestion by @mhegazy at https://github.com/Microsoft/TypeScript/issues/16503#issuecomment-308250188, such as to specify known length. I'd imagine the distinct length literals would prevent one from assigning higher-length tuples to lower-length ones, as suggested here.

interface Tuple<TLength extends number, TUnion> extends ReadonlyArray<TUnion> {
    length: TLength;
}

The obvious question here seems whether ending this assignability would break much in practice. Seems worth finding out.

Then again though, in other areas like implicit JS casts like Number -> String TS's stance appears to have been that explicit conversions beat implicit magic, and I suppose it might not be unreasonable to extend that reasoning to this tuple case as well.

KiaraGrouwstra commented 7 years ago

I've just opened a WIP PR based on the explicit length idea at #17765. I think I'm half-way, but feel a bit stuck about how to properly get the tuples to derive from this interface; input welcome.

KiaraGrouwstra commented 7 years ago

Update: got it to work. Using a flag for those concerned about breaking change, so should be win-win.

mstn commented 6 years ago

@tycho01 I used your trick with length here and I was able to define a type for arrays of generic fixed size. It is a bit hacky, but it seems to work in some common use cases. I do not know if this application is known or if it can be reduced to your work without defining a new interface as I did.

KiaraGrouwstra commented 6 years ago

@mstn: I hadn't tried that -- I've no idea how your 0 workaround managed to beat the type widening issue! That said, it seems to work also as the simplified type FixedSizeArray<N extends number, T> = { 0: any, length: N } & ReadonlyArray<T>?

mstn commented 6 years ago

Yes, you are right. Actually, the default 0 for M yields nothing else but { 0: any }!

The trick works only for tuple and not for the corresponding objects. Moreover, it works only with 0 (or a sequence 0, 1, ...) and not with non "sequential" keys. It fails for tuple types, of course.

Is it a bug or a feature?

type A = { 0: any };

let a1: A = ['a', 'b']; // ok
let a2: A = { 0: 'a', 1: 'b' }; // error

type B = { 1: any };

let b1: B = ['a', 'b']; // error
let b2: B = { 0: 'a', 1: 'b' }; // error

type C = { 0: any, 1: any };

let c1: C = ['a', 'b', 'c']; // ok
let c2: C = { 0: 'a', 1: 'b', 2: 'c' }; // error

type D = [any];

let d1: D = ['a']; // ok
let d2: D = ['a', 'b']; // error
mstn commented 6 years ago

If we think in Javascript, an array is an object with sequential numerical keys. Hence, expressions like a1 or c1 are a sort of upcasting. The Typescript compiler is smart enough to understand it! So I think it should be a feature. What do you think?