#generic function return type narrowing
58 messages · Page 1 of 1 (latest)
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const;
type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes;
styles?: Styles;
options?: Options;
}
declare const config: Config
declare const tabSelection: number
const handleStylesConfigChanged = (eventConfig: {}, config: Styles) => { return {...eventConfig,...config} }
const propertyKey = editorConfigProperties[tabSelection];
const getGlobalConfigProperty = (
// ^? - const getGlobalConfigProperty: (configProperty: (typeof editorConfigProperties)[number]) => Attributes
configProperty: typeof editorConfigProperties[number],
) => {
return config?.[configProperty] ?? {}
};
locally i get this:
const getGlobalConfigProperty: (configProperty: (typeof editorConfigProperties)[number]) => Attributes | Styles | Options
but im trying to get a simple reproducible setup in the playground for my real question about generic return type narrowing.
I get the same than you do in the playground ..
Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const
type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}
declare const config: Config
...```
Ok so about your question..
configProperty: typeof editorConfigProperties[number],
is of type configProperty: "attributes" | "styles" | "options"
therefore,
config?.[configProperty]
is of type
Config['attributes'] & Config['styles'] & Config['options']
Since they are all equal, and empty, in your example, well, typescript just picks one of them, such as Attributes. Fun fact, on the playground, if you reverse the declaration order of the types Attributes and Styles.. you will get a different "result".
Now if you want to narrow, you need to specify the type of the key
But there is a first issue.. you may not be able to assign {} to Attrribute/Styles/Options, so you need for the client to pass the default values. or to create a global config object for default values..
Then you can do this.
Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const
type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}
declare const config: Config
...```
shouldnt attr and attr1 error here because they are not Style types?
Preview:ts ... const attr = propertyKey === "attributes" ? handleStylesConfigChanged( {}, globalPropertyConfig ) : {} ...
the reason i was trying to reproduce the playground thing is because in my code the types(slightly differnt names from the playground) for getGlobalConfigProperty1 are returned like i assume they would:
sorry for the pictures. tired to avoid that but couldnt get it to show like i am seeing locally.
ok so i tried just casting the {} as the type if its undefined but somehow its still carrying around undefined in the return type. which then cant be passed to handleStylesConfigChanged 🤔
Preview:```ts
...
const getGlobalConfigProperty1 = <
TProp extends typeof editorConfigProperties[number]
(
// ^? - const getGlobalConfigProperty: (configProperty: (typeof editorConfigProperties)[number]) => Attributes
configProperty: TProp
): Config[TProp] => {
return (
config?.[configProperty] ?? ({} as Config[TProp])
)
}
...```
Yus, it's because it is returning a TConfig[TProp] and in your original Config types, all fields are optional.
You can solve this by using NonNullable
Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const
type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}
declare const config: Config
...```
oh interesting. is it worse practice to cast in the return as in my function vs the way you did it by adding a parameter and typing it there?
Casting is bad when it brings false promises. and in your case, casting {} into anything will be risky: every fields of Attribute/Styles/Config wont be trustable since they can be returned as undefined
If you dont want to pass a default value, you can define a default value record.. gimme a minute..
Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const
type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}
type DefaultValueType = {
[k in keyof Config]: NonNullable<Config[k]>
}
...```
does this apply if im using exactOptionalPropertyTypes?
You can click on the playground and tick any typescript flag, to check if it works with your setup 🙂
click TS COnfig, look for this, toggle it and check !
oh ya, i mean when doing {} as NonNullable <Config[TProp]>, why would i need default values if the optional properties cant be undefined?
I let you see what happens in this case
Preview:```ts
const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const
type Attributes = {}
type Styles = {
fontStyles: {
family: string
size: number
}
}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Style
...```
There is no type warning, while at the runtime, you will get a surprise.
why is it being casted and set as the return value?
const getGlobalConfigProperty1 = <TProp extends typeof editorConfigProperties[number]>(
configProperty: TProp,
): NonNullable<Config[TProp]> => { // <---
return config?.[configProperty] ?? ({} as NonNullable <Config[TProp]>) // <---
};
Can you rephrase your question ? I do not understand what you mean
rephrased and annotated
well you cannot just turn {} into something that may demand more properties. Either you give it the needed properties, either you cheat by casting it
i dont follow, are you talking about the default values? im talking about the return and casting:
const getGlobalConfigProperty1 = <TProp extends typeof editorConfigProperties[number]>(
configProperty: TProp,
): NonNullable<Config[TProp]> => {
return config?.[configProperty] ?? ({} as NonNullable <Config[TProp]>)
};
vs
const getGlobalConfigProperty1 = <TProp extends typeof editorConfigProperties[number]>(
configProperty: TProp,
) => {
return config?.[configProperty] ?? ({} as NonNullable <Config[TProp]>)
};
Ah, you can definitely remove the return type of the function if you want ! But you will still need to cast, because {} is not a NonNullable<Config[TProp]>
its weird that the type is being returned properly here but its able to be passed into handleStylesConfigChanged with the incorrect type:
the type returned is non nullable, so it is of correct typings.
shouldnt the second parameter only take a Style type?
const handleStylesConfigChanged = (eventConfig: {}, config: Styles) => {}
Ah ! Indeed. But in your playground, types Style / Attributes / Options are actually equal. In typescript, types/interfaces are not strong.
Add a specific property to your Styles type and watch what happens.
"are not strong"?
Types are contracts.
type Duck = {
quack:()=>void
}
Here you are telling typescript "everything that can quack is a duck"
by doing
type Attributes = {}
type Styles = {}
type Options = {}
You are telling typescript "any non-null and non-undefined object is an attribute, a style, and an option at the same time"
each type isnt a unique contract?
Nope ! types are basically aliases . You are giving a name to a construct.
In your example, you can do
const attributes:Attributes = new Date():
const styles:Styles = new Error();
const options:Options = styles
and it works fine ! they are actually different aliases for {}
in my app where im applying this the contracts are:
export interface Attributes {
label?: string;
icon?: string;
isFadingIndicator?: boolean;
minWidth?: boolean;
isMinWidthIndicator?: boolean;
stacked?: boolean;
}
export interface Styles
extends Partial<DefaultConfigColorProperties>,
Partial<DefaultConfigNonColorProperties> {
[x: string]: string;
}
export interface Options {
defaultTabIndex?: number;
tabsAtTop?: boolean;
}
i guess i need to figure out if i need to handle the default values like you pointed out above: #1080572434094227567 message
because {} is valid for interface Styles as shown above.
Yus ! You can check that with this
type Test = {} extends Styles? true: false;
const test:Test = true
thanks for your help @vocal edge! im going to mark as resolved. i might post back so hopefully you dont mind the ping.
!resolved
if i wanted to preemptively get the global config based on the propertyKey and store it in globalPropertyConfig, why doesnt the type carry through when narrowing with propertyKey === "styles"? if i just use getGlobalConfigProperty(propertyKey) directly it carries fine as shown in the bottom two examples:
Preview:ts ... const changedConfig = propertyKey === "attributes" ? handleStylesConfigChanged( {}, globalPropertyConfig ) : {} const changedConfig1 = propertyKey === "styles" ? handleStylesConfigChanged( {}, globalPropertyConfig ) : {} ...
im also confused about the NonNullable return type here. why is it showing NonNullable at all and also carrying the undefined further?:
const globalPropertyConfig: NonNullable<Attributes | Styles | Options | undefined>