ts-belt icon indicating copy to clipboard operation
ts-belt copied to clipboard

F.getWithDefault Coercion type problem

Open mattaiod opened this issue 2 years ago • 5 comments

Hello everyone, first : thanks for this incredible library, look like the best in functionnal programmin in Ts

In the basic example

const res = pipe(
  [1, 2, 3, 4, 5], // → [1, 2, 3, 4, 5]
  A.dropExactly(2), // → Some([3, 4, 5])
  O.flatMap(A.head), // → Some(3)
  O.map(N.multiply(10)), // → Some(30)
) // → 30

O.getWithDefault(0)(res) // → 30

I have an typescript error at the last line:

Argument of type 'Option<number>' is not assignable to parameter of type 'Option<0>'.
  Type 'number' is not assignable to type 'Option<0>'.ts(2345)

The only way I have to fix that is to explicit precise the type like: O.getWithDefault(0 as number)(res) // → 30

The version not curried works: O.getWithDefault(res, 0)

I guess it's not normal so what is the reason and the solution?

Here my .eslintrc :

{ "extends": "@antfu", "plugins": [ "functional" ], "rules": { "@typescript-eslint/quotes": 0, "no-console": "off", "array-callback-return": "error", "no-constructor-return": "error", "no-duplicate-imports": "off", "no-new-native-nonconstructor": "error", "no-self-compare": "error", "no-template-curly-in-string": "error", "no-unused-private-class-members": "error", "class-methods-use-this": "error", "consistent-return": "error", "default-case": "error", "dot-notation": "error", "eqeqeq": "error", "init-declarations": "error", "no-eq-null": "error", "no-extend-native": "error", "no-implicit-coercion": "error", "@typescript-eslint/no-unused-vars": "off", "no-implicit-globals": "error", "no-new": "error", "no-new-func": "error", "no-new-object": "error", "no-var": "error", "prefer-object-spread": "error", "require-await": "error", "yoda": "error", "explicit-function-return-type": "off" } }

tsconfig.json:

{ "compilerOptions": { "noUnusedLocals": true, "noUnusedParameters": true, "baseUrl": ".", "module": "ESNext", "target": "ESNext", "lib": [ "DOM", "ESNext", "WebWorker" ], "strict": true, "esModuleInterop": true, "jsx": "preserve", "skipLibCheck": true, "moduleResolution": "node", "resolveJsonModule": true, "incremental": false, "noImplicitOverride": true, "noImplicitAny": true, "noImplicitThis": true, "strictFunctionTypes": true, "alwaysStrict": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "useUnknownInCatchVariables": true, "allowUnusedLabels": false, "allowUnreachableCode": false, "noUncheckedIndexedAccess": true, "noPropertyAccessFromIndexSignature": true, "noFallthroughCasesInSwitch": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true, "strictNullChecks": true, "allowJs": false, "forceConsistentCasingInFileNames": true, "types": [ "vitest", "vite/client", "vue/ref-macros", "vite-plugin-pages/client", "vite-plugin-vue-layouts/client", ], "paths": { "~/": [ "src/" ] } }, "exclude": [ "dist", "node_modules", "cypress", "auto-imports.d.ts" ] }

Thank you in advance for your attention

mattaiod avatar Jul 12 '23 16:07 mattaiod

give type parameter to O.getWithDefault. like

O.getWithDefault<number>(0).

JUSTIVE avatar Jul 19 '23 03:07 JUSTIVE

@JUSTIVE Yes, I'm familiar with this solution, but I'd like to know if there's a way to improve this function to have this default behavior.

mattaiod avatar Jul 19 '23 11:07 mattaiod

@mattaiod you're issue above is because O.getWithDefault expects 0 to be an Option<number> type but it is a literal integer, take a look at the signature for why the non-curried version works:

export declare function getWithDefault<A>(option: Option<A>, defaultValue: NonNullable<A>): A;
export declare function getWithDefault<A>(defaultValue: NonNullable<A>): (option: Option<A>) => A;

As you see, the function would explicitly type A as 0 in the curried version whereas it would resolve to just number in the other, because normally the curried value would be within the pipe already and that would define the type. Basically don't use the curried version outside of a pipe/flow sequence OR type it yourself.

Note, there are a lot of instances where you need to do this when using getWithDefault. For example, the following would be a type error because the default is never[]

pipe(O.fromNullable([1, 2, 3]), O.getWithDefault([]))

probablykabari avatar Sep 25 '23 16:09 probablykabari

How about making the default type an extended type of the given T type when it's curried? I just made a proof-of-concept version of this. image https://www.typescriptlang.org/play?strictNullChecks=false&jsx=0&ts=5.4.0-dev.20240121#code/JYWwDg9gTgLgBAbzMMBTANAeQL5wGZQQhwBEAAiBAEbAA2AngPQwDOAtFarTCQFAz00cTGBjAIAOwA8AFQB8cALxwZcAD5wJAV1q11cLRIAmqPMAmojvXo0YBjLVCioJ8UGFqoQLmAEMxkryoAB6QsHB2kizwAOaoMADqwDAAFgAipr468MqycgAUJnhZ3ABqvrRaqABcAHKStTq0vlSeeQCUigXB1SIB0vKdcrxwo3DBAPw9RSUw5ZWo1rZghJAslnDunt6u-uISQaHQ8JES0XBxicnpmdkATEpwsugAGnAhMC5GLHD1Eo26FpteSKP4A5qtVB5AozbLzKrVF5DfI9Pr7DpdEZjSbTW5lCpVawCIQASU+xGUJF8JH0JCoNLUJDsfBs9kczlcm3A2x8e0CyDQ+SxmAAdABlIiofJUkjtdCs0YAPQmWMuSVSGWK2WlzPaWNsyt4etZKwgaw2Wy8vP6vAFUuF4slUjJXgKMrlCrghtGauumtmdx1stZhvaQA

JUSTIVE avatar Jan 21 '24 18:01 JUSTIVE

Maybe the NoInfer utility type from the Typescript 5.4.0 would be solution for this. updated example above, seems just working as intended

JUSTIVE avatar Feb 11 '24 11:02 JUSTIVE