#Type aliasing - weird behavior

22 messages · Page 1 of 1 (latest)

blissful cairn
#

I've noticed the following behavior in typescript that seems like a bug. If it is not, what is the reason aliasing the type changes things?

type Item = { id: number };

type Container<T extends Record<string, Item>> = {
  name: string,
  items: T,
  pickedItemKeys?: (keyof T)[],
}

function container<T extends Record<string, Item>>(
  options: Container<T>
): Container<T> {
  return {
    ...options
  }
}

const a = container({
  name: 'test',
  items: {
    a: { id: 1 },
    b: { id: 2 },
  },
  pickedItemKeys: ['a']
});

// here's the weird part

function consumeContainer(container: Container<Record<string, Item>>) {
  Object.entries(container.items).forEach(([k, v]) => console.log(v.id));
}

// does not compile - it's the pickedItemKeys that's setting it off
consumeContainer(a);

// but if I define a type alias for the type used as the argument in the above fn
type UntypedContainer = Container<Record<string, Item>>;

function consumeContainer2(container: UntypedContainer) {
  Object.entries(container.items).forEach(([k, v]) => console.log(v.id));
}

// it works fine ¯\_(ツ)_/¯
consumeContainer2(a);
glossy apex
#

but if I define a type alias for the type used as the argument in the above fn
it works fine ¯_(ツ)_/¯
it doesn't

slender pollenBOT
#
ascor8522#0

Preview:```ts
interface Item {
id: number
}

interface Container<T extends Record<string, Item>> {
name: string
items: T
pickedItemKeys?: (keyof T)[]
}

function container<T extends Record<string, Item>>(
options: Container<T>
): Container<T> {
r
...```

glossy apex
#

!ts

slender pollenBOT
#
interface Item {
    id: number;
}

interface Container<T extends Record<string, Item>> {
    name: string;
    items: T;
    pickedItemKeys?: (keyof T)[];
}

function container<T extends Record<string, Item>>(options: Container<T>): Container<T> {
    return {
        ...options,
    };
}

const a = container({
    name: 'test',
    items: {
        a: { id: 1 },
        b: { id: 2 },
    },
    pickedItemKeys: ['a'],
});

// here's the weird part

function consumeContainer(container: Container<Record<string, Item>>) {
    Object
        .entries(container.items)
//       ^^^^^^^
// Property 'entries' does not exist on type 'ObjectConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2017' or later.
        .forEach(([k, v]) => console.log(v.id));
//                 ^  ^
// Binding element 'k' implicitly has an 'any' type.
// Binding element 'v' implicitly has an 'any' type.
}
consumeContainer(a);
//               ^
// Argument of type 'Container<{ a: { id: number; }; b: { id: number; }; }>' is not assignable to parameter of type 'Container<Record<string, Item>>'.
//   Type 'Record<string, Item>' is missing the following properties from type '{ a: { id: number; }; b: { id: number; }; }': a, b

type UntypedContainer = Container<Record<string, Item>>;
function consumeContainer2(container: UntypedContainer) {
    Object
        .entries(container.items)
//       ^^^^^^^
// Property 'entries' does not exist on type 'ObjectConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2017' or later.
        .forEach(([k, v]) => console.log(v.id));
//                 ^  ^
// Binding element 'k' implicitly has an 'any' type.
// Binding element 'v' implicitly has an 'any' type.
}
consumeContainer2(a);
//                ^
// Argument of type 'Container<{ a: { id: number; }; b: { id: number; }; }>' is not assignable to parameter of type 'UntypedContainer'.
glossy apex
#

(ignore the error with Object.entries, the default setting of the bot are kinda outdated)

#

but you can see an error happens with both invocations

#

both having the same source/meaning: Container<{ a: { id: number; }; b: { id: number; }; }> not being assignable to Container<Record<string, Item>>

#

@blissful cairn also, your container function doesn't do "anything", but makes it harder for TS to keep track of the exact type of the object
if you remove it and simply call your consumeContainer function with the raw object, it will work just fine

slender pollenBOT
#
ascor8522#0

Preview:```ts
// @target: esnext

interface Item {
id: number;
}

interface Container<T extends Record<string, Item>> {
name: string;
items: T;
pickedItemKeys?: (keyof T)[];
}

const a = {
name: 'test',
items: {
a: { id: 1 },
b: { id: 2 },
...```

glossy apex
#

!ts

slender pollenBOT
#

interface Item {
    id: number;
}

interface Container<T extends Record<string, Item>> {
    name: string;
    items: T;
    pickedItemKeys?: (keyof T)[];
}

const a = {
    name: 'test',
    items: {
        a: { id: 1 },
        b: { id: 2 },
    },
    pickedItemKeys: ['a'],
};

function consumeContainer(container: Container<Record<string, Item>>) {
    Object
        .entries(container.items)
        .forEach(([k, v]) => console.log(v.id));
}
consumeContainer(a);

type UntypedContainer = Container<Record<string, Item>>;
function consumeContainer2(container: UntypedContainer) {
    Object
        .entries(container.items)
        .forEach(([k, v]) => console.log(v.id));
}
consumeContainer2(a);
glossy apex
#

no errors
it not any less type-safe either, since if the a variable were to be a different type, you'd get an error when trying to call consumeContainer

#

only difference is that you don't mess with generics and break the exact type of the object

blissful cairn
# glossy apex > but if I define a type alias for the type used as the argument in the above fn...
slender pollenBOT
#

@blissful cairn Here's a shortened URL of your playground link! You can remove the full link from your message.

voightkampff#0

Preview:```ts
type Item = {id: number}

type Container<T extends Record<string, Item>> = {
name: string
items: T
pickedItemKeys?: (keyof T)[]
}

function container<T extends Record<string, Item>>(
options: Container<T>
): Container<T> {
return {
...options,
...```

vagrant meadow
#

I can confirm that this error happens to me as well.

nimble cape
#

By not using a generic parameter though, both consumeContainer function are broken as they allow invalid Containers.

#

Container<Record<string, Item>> means that your type is now more or less:

type Container = {
  name: string,
  items: {[k: string]: Item },
  pickedItemKeys?: string[],
}

so your consumerContainer functions now allow:

consumeContainer({
  name: 'test',
  items: {
    a: { id: 1 },
    b: { id: 2 },
  },
  pickedItemKeys: ['Hello', 'World']
});
nimble cape
#

As for your alias problem, I've seen it before.. it's related to evalation order in the TS compiler. They're usually not a problem but in some rare cases, they can be.

#

Here's a classic example:

slender pollenBOT
#
.rcyr#0

Preview:```ts
type MyJSONType =
| string
| number
| boolean
| null
| MyJSONType[]
| {[k: string]: MyJSONType}

type MyJSONType2 =
| string
| number
| boolean
| null
| MyJSONType2[]
| Record<string, MyJSONType2>```