#Create an object type without a certain key

1 messages · Page 1 of 1 (latest)

waxen finch
#

Hi! Im trying to create a type which is an object and can hold any key except "data"

type ObjectWithoutData = {
            [key: string]: any;
        } & {
            data?: never;
        };

const x: ObjectWithoutData = {
    test: "hello",
    data: "test" // ERRORS which is what I want!
};

let y: any = {}

y["test"] = x.data; // Doesnt error :/

I've got something like this but you can still access x.data, now I know that the assignment works because it's optional but if you make it not optional then you need to include it which I also dont want.... is there any solution to this?

soft grove
#
type ObjectWithoutData = Omit<{
  [key: string]: any;
}, "data">;
waxen finch
#

doesnt work 😄

soft grove
waxen finch
#

Well none which is the problem, you can use data without any errors

#
        type ObjectWithoutData = Omit<
            {
                [key: string]: any;
            },
            "data"
        >;

        const x: ObjectWithoutData = {
            test: "hello",
            data: "test",
        };

        let y: any = {};
        y["test"] = x.data;
#

no errors

slate flicker
#

It's fundamentally impossible in TS to write a type that disallows a property entirely

#

That data?: never is a clever hack that makes it impossible to assign it an object that has a data property because there is no way to create such a value.

#

But all you are really saying is that data is a property of the bottom type never

#

so yes it only blocks assignment to the type, but reading from it works fine and you even get a type that's assignable to anything (after you remove the undefined)

#

(and purely looking at type-land you can even still assign to this property:)

maiden swallowBOT
#
angryzor#0

Preview:```ts
import process from "process"

const x: {foo: never} = {foo: process.exit()}```

slate flicker
#

(personally I really don't like this hack to be honest but it gets spread everywhere)

neon cave
#

You can use a generic identity function and validate it doesn't have data property.

#
type DisallowData<T> = {
    [K in keyof T]: K extends 'data' ? never : T[K]
}

const disallowData = <T>(obj: DisallowData<T>) => obj

disallowData({})
disallowData({ foo: 123 })
disallowData({ data: 456 })
disallowData({ foo: 123, data: 456 })
#

!ts

maiden swallowBOT
#
type DisallowData<T> = {
    [K in keyof T]: K extends 'data' ? never : T[K]
}

const disallowData = <T>(obj: DisallowData<T>) => obj

disallowData({})
disallowData({ foo: 123 })
disallowData({ data: 456 })
//             ^^^^
// Type 'number' is not assignable to type 'never'.
disallowData({ foo: 123, data: 456 })
//                       ^^^^
// Type 'number' is not assignable to type 'never'.
neon cave
#

But even this can be worked around.

slate flicker
#

Thought of that too but that will only work of they allow all result objects to be inferred

#

Because you can't define a result type that disallows data:

maiden swallowBOT
#
angryzor#0

Preview:```ts
type DisallowData<T> = {
[K in keyof T]: K extends "data" ? never : T[K]
}

type F = DisallowData<{[key: string]: any}>
// ^?```

slate flicker
#

(from being read)

neon cave
#

Yeah you have to use the disallowData function, can't use the naked type.

#

So instead of:

const foo: ? = { ... }

You have to do:

const foo = disallowData({ ... })

(And even that can still be worked around)

#

Depends on why you want to disallow data, there might be better ways.

waxen finch
#

I wanted to have something like this if it makes sense

type ObjectWithoutData = {
            [key: string]: any;
        } & {
            data?: never;
        };

type ObjectWithData = Record<string, any>;

// response from api either contains an object with a data section or the object is missing the data section
const myApiData: ObjectWithoutData | ObjectWithData = {}

// imagine this to be a  like an app store
const store: Record<string, any> = {}

// type guard
if (myApiData.data) {
    // I want to be able to access data here cleanly
    store["data"] = myApiData.data // should work
} else {
    // i dont want to be able to access data here and if i do so get an error.
    store["something2"] = myApiData.data // should error
}
#

and no, I dont know how the data looks like, I only know that it either has a data property in the object or it does not and I kinda want that typed

neon cave
#

What other keys does it have besides the data?

waxen finch
#

not known at compile time

#

because otherwise i would just create two types where data is missing in one of them but yea :/

neon cave
#

If you don’t know anything about the object keys, then data is no different from any other key.

#

Just use myApiData: object, and you already have to do 'data' in myApiData check before you use it.

waxen finch
#

still wont prevent anyone from trying to access data in the else

neon cave
#

It will

#
declare const api: object

if ('data' in api) {
  api.data
} else {
  api.data
}
#

!ts

maiden swallowBOT
#
declare const api: object

if ('data' in api) {
  api.data
} else {
  api.data
//    ^^^^
// Property 'data' does not exist on type 'object'.
}
waxen finch
#

oh wow xd

#

well that solves it then i guess

#

didnt even have to try anything fancy

neon cave
#

You problem was that

#

{ [k: string]: any } is the wrong type to begin with.

#

That type means “any string is a valid property” and that’s not true.

#

Because any string is a valid property, you can access anything, not just api.data but also api.beef and api.salsa.

waxen finch
#

but i still find it a bit weird that this doesnt work

#
declare const api: Record<string, any>;

if ('data' in api) {
  api.data
} else {
  api.data
}
#

!ts

maiden swallowBOT
#
declare const api: Record<string, any>;

if ('data' in api) {
  api.data
} else {
  api.data
}
neon cave
#

It works, any key is a valid property no matter if it passes the check or not, it’s still valid.

#

What you really want is “I don’t know if any key exists at all so force me to check it first”

waxen finch
#

how would i solve this?

#

const store: Record<string, any> = {}
declare const api: object

if ('data' in api) {
  store["data"] = api.data[0];
} else {
}
#

!ts

maiden swallowBOT
#

const store: Record<string, any> = {}
declare const api: object

if ('data' in api) {
  store["data"] = api.data[0];
//                ^^^^^^^^
// 'api.data' is of type 'unknown'.
} else {
}
waxen finch
#

just as any?

neon cave
#

You can’t access api.data[0] unless you know it’s an array, so check if it’s an array using Array.isArray.

#

Didn’t you say you don’t know anything about the API? How do you know data might exist and it might be an array and you want the first element?

#

Because “I don’t know anything about its shape” is very different from “I know its shape I’m just not sure if it will be there”

waxen finch
#

I know the rough shape of data if it exists

neon cave
#

Describe it.

waxen finch
#

In the end it’s just an array of Record<string, any> 😂

neon cave
#

Well you certainly know more than that, otherwise how are you going to use api.data[0]?

waxen finch
#

Using [0] was just an example, basically I wanted to know if there’s a way to type it a bit more even when it’s an object

neon cave
#

Then use the same narrowing of key in object or Array.isArray.

waxen finch
#

Okay 👍

#

Thanks

neon cave
#

But really, if you know api.data.salsa might be there, you can do way better:

declare const api: {
  data?: {
    salsa?: string
  }
}

if (api.data?.salsa) {
  api.data.salsa
} else {
  api.data.salsa
}
#

!ts

maiden swallowBOT
#
declare const api: {
  data?: {
    salsa?: string
  }
}

if (api.data?.salsa) {
  api.data.salsa
} else {
  api.data.salsa
//^^^^^^^^
// 'api.data' is possibly 'undefined'.
}